如何使实体框架对每条记录使用 1 个更新查询而不是 1 个?

Posted

技术标签:

【中文标题】如何使实体框架对每条记录使用 1 个更新查询而不是 1 个?【英文标题】:How to make Entity Framework use 1 update query instead of 1 for each record? 【发布时间】:2021-09-19 14:24:23 【问题描述】:

我有这个代码:

var query = _calendarDayRepo.Queryable().Where(x => x.Date >= calendarDayDto.Date);

foreach (var calendarDay in query)

    calendarDay.WorkingDayCounter += (updatedEntityAdd ? 1 : -1);


await _calendarDayRepo.SaveChangesAsync();  

我正在使用 SQL Server 数据库。

目前,EF 将执行一个查询来更新每条记录,但我相信如果 EF 只生成 1 个查询来一次更新所有记录,性能会更高。

我怎样才能做到这一点?

【问题讨论】:

你可能想看看EntityFramework.Extended for batch update & delete 见EF Core Tools & Extensions - 寻找术语“更新” 我推荐一个更强大的 ORM:linq2db。看看在其中做一个update 是多么容易。 【参考方案1】:

使用实体框架时,有两种获取数据的方法。其中只有一个用于获取需要更新的项目。

如果您使用 Select 查询数据,则使用 SQL Select ... From ... 等执行查询。获取的数据将返回给您。 DbContext 不会保存此数据。如果您不打算更新获取的数据,则此方法是首选方法,因为它是这种用法最有效的方法

如果您想使用实体框架更新项目,正确的方法是获取要更新的数据,更新需要更改的属性并调用SaveChanges。后者将进行实际更新。

要使用此方法,您需要查询竞争对象,而不使用 Select。 DbContext 有一个 ChangeTracker。如果您执行这样的查询,获取的对象将被放入 ChangeTracker,以及它的副本。你得到对副本的引用(或者可能是原件,哪个无关紧要)。因为您有引用,所以更改属性将更改 ChangeTracker 中的副本。当您调用 SaveChanges 时,ChangeTracker 中的所有原件都会按值与其副本进行比较。更改将转换为 SQL Update ... Set ... Where ... 语句。

到目前为止,您应该清楚地看到,如果不使用 Select 来获取不打算更新的项目,效率会很低。

使用实体框架时,请始终使用Select 来获取项目,并仅选择您实际计划使用的属性。仅在没有 Select 的情况下获取,如果您打算更新获取的项目,请仅使用 Include

示例

假设您有一个包含客户、产品、订单等的数据库。客户和订单之间是一对多的,产品和订单之间是一对多的。

“给我所有尚未付款的订单的地址和其他一些信息,因为我想给他们发送提醒”

没有更新,使用 Select。仅获取您实际计划使用的项目。

DateTime dueDate = DateTime.Today - TimeSpan.FromDays(14); // 2 weeks ago
using (var dbContext = new OrderContext(...))

    var ordersThatNeedAReminder = dbContext.Orders
        .Where(order => !order.Paid && order.DueDate <= dueDate)
        .Select(order => new
        
             Customer = new
             
                 // only select the Customer Properties that you plan to use
                 Name = order.Customer.Name,
                 AddressLine1 = order.Customer.Address.AddressLine1,
                 ...

                 // probably not needed:
                 // Id = customer.Id
             ,

             Order = new
             
                 DeliveryDate = order.DeliveryDate,
                 PayDate = order.PayDate,
                 Amount = order.Total,
                 ...
             ,
       )
       .ToList();

       ProcessNonPaidOrders(ordersThatNeedAReminder);

因为我使用Select获取所有项目,所以获取的项目没有放在ChangeTracker中。

“所有割草机都需要调整价格”

using (var dbContext = new OrderContext(...))

    // Fetch the lawnMowers
    IEnumerable<Product> lawnMowers = dbContext.Products
        .Where(product => product.Type == ProductType.LawnMower)
        .ToList();

    UpdatePrice(lawnMowers, ...);    // not part of the question

    DbContext.SaveChanges();

因为查询数据时没有Select,所以把取到的item和一个copy放到ChangeTracker中。您会获得对副本的引用,该副本的属性 Price 已更改。 SaveChanges 将比较 ChangeTracker 中所有项目的 Original 和 Copy,并为所有更改的属性创建适当的 SQL 更新。

此方法是执行更新的首选方法。

优势:您可以在更改之前获得该项目的最新版本。 缺点:您必须获取完整的项目(不是通过外键引用的项目,除非它们也需要更新)。

有一种方法可以立即将更改项的值放入 ChangeTracker 中。

更新 Husqvarna 割草机的价格

long husqvarnaLawnMowerId = ...
Product lawnMowerHusqvarna = FetchProduct(husqvaranLawnMowerId);
decimal adjustedPrice = AskOperatorForNewPrice(lawnMowerHusqvarna);

lawnMowerHusqvarna.Price = adjustedPrice;

using (var dbContext = new OrderContext(...))

    dbContext.Products.Attach(lawnMowerHusqvarna);
    dbContext.Savechanges();

因此,如果您已经知道要更新的对象的所有字段的值,则不必先获取它们。

但请注意,其他人可能已经更改了一个或多个属性。获取当前值后,您将丢失您自己或其他人所做的更改。

因此我不推荐这种方法。通常,与查询数量相比,对数据库的更改数量相对较少。通常这些更改是在操作员输入之后进行的,因此这些更改不必非常快。我的建议是:不要使用这种方法。

【讨论】:

以上是关于如何使实体框架对每条记录使用 1 个更新查询而不是 1 个?的主要内容,如果未能解决你的问题,请参考以下文章

如何使Readity实体框架数据上下文

谈ENTITYFRAMEWORK数据更新之技巧

实体框架:开发不执行更新和删除的模型

从多个更新查询中返回一个“更新的记录”

在实体框架中使用更新数据库时子查询返回超过 1 个值错误

如何使用实体框架向数据库服务器询问当前日期时间?