Entity Framework Core 中的批量更新

15

我从数据库中获取一堆时间表条目并使用它们创建发票。一旦我保存了发票并拥有一个Id,我想使用发票Id更新时间表条目。有没有一种批量更新实体而无需逐个加载它们的方法?

void SaveInvoice(Invoice invoice, int[] timeEntryIds) {
    context.Invoices.Add(invoice);
    context.SaveChanges();

    // Is there anything like?
    context.TimeEntries
        .Where(te => timeEntryIds.Contains(te.Id))
        .Update(te => te.InvoiceId = invoice.Id);
}

有一个库可以用来实现这个功能:https://github.com/borisdj/EFCore.BulkExtensions - borisdj
8个回答

18

免责声明:我是项目Entity Framework Plus的所有者。

我们的库拥有批量更新功能,我相信这正是您所需要的。

此功能支持EF Core。

// Is there anything like? YES!!!
context.TimeEntries
    .Where(te => timeEntryIds.Contains(te.Id))
    .Update(te => new TimeEntry() { InvoiceId = invoice.Id });

维基: EF批量更新

编辑:回答评论

它支持像您示例中的包含吗?我认为这来自EF Core,这是3.1版本中不支持的功能

EF Core 3.x支持包含:https://dotnetfiddle.net/DAdIO2

编辑:回答评论

这很棒,但这需要类具有零参数公共构造函数。 这不是一个好的选择,有什么方法可以解决这个问题吗?

匿名类型从EF Core 3.x开始支持

context.TimeEntries
    .Where(te => timeEntryIds.Contains(te.Id))
    .Update(te => new { InvoiceId = invoice.Id });

在线示例:https://dotnetfiddle.net/MAnPvw


这很好,但是这要求类必须有零参数的公共构造函数,这并不是一个好的做法。有什么方法可以解决这个问题吗? - akd

16

从 EFCore 7.0 开始,您将看到内置的 BulkUpdate()BulkDelete() 方法:

   context.Customers.Where(...).ExecuteDelete();
   context.Customers.Where(...).ExecuteUpdate(c => new Customer { Age = c.Age + 1 });
   context.Customers.Where(...).ExecuteUpdate(c => new { Age = c.Age + 1 });
   context.Customers.Where(...).ExecuteUpdate(c => c.SetProperty(b => b.Age, b => b.Age + 1));

4
给任何发现这个信息的人一个提示:EFCore 7 还未发布,预计将于2022年11月发布。 - Jono
1
API在最终版本中是ExecuteDelete()/ExecuteUpdate()。 - Rm558
ExecuteDelete() / ExecuteUpdate() 与 InMemory 不兼容,请查看此处 https://dev59.com/QMXsa4cB1Zd3GeqPo6XV - Murali Murugesan

8
你是否需要简化语法的高性能?我建议使用直接的SQL查询。
 string query = "Update TimeEntries Set InvoiceId = <invoiceId> Where Id in (comma separated ids)";    
 context.Database.ExecuteSqlCommandAsync(query);

对于逗号分隔的id,你可以使用string.Join(',', timeEntryIds)

这取决于你实际需要什么。如果你想使用Linq,那么你需要遍历每个对象。


SQL注入攻击!!每个使用EF的人都知道他们可以发出SQL,但这并不推荐。 - Akash Kava
1
微软自己的 EF 文档建议使用原始 SQL 插入进行批量更新以提高效率:https://learn.microsoft.com/en-us/ef/core/performance/efficient-updating - MDave

6
如果 TimeEntry 关联到 Invoice(检查导航属性),你可以尝试这样做:
var timeEntries = context.TimeEntries.Where(t => timeEntryIds.Contains(te.Id)).ToArray();

foreach(var timeEntry in timeEntries)
    invoice.TimeEntries.Add(timeEntry);

context.Invoices.Add(invoice);

//save the entire context and takes care of the ids
context.SaveChanges();

我没有按照原计划那样做,而是在保存发票之前将TimeEntries添加到了发票中。invoice.TimeEntries = context.TimeEntries.Where(te => timeEntryIds.Contains(te.Id)).ToArray() - Adrian Brand

4
在Entity Framework Core 5.0中引入了IQueryable.ToQueryString方法,可以帮助处理此情况。该方法将生成SQL,可以包含在原生SQL查询中,用于执行由该查询标识的记录的批量更新。
例如:
void SaveInvoice(Invoice invoice, int[] timeEntryIds) {
    context.Invoices.Add(invoice);
    context.SaveChanges();

    var query = context.TimeEntries
        .Where(te => timeEntryIds.Contains(te.Id))
        .Select(te => te.Id);

    var sql = $"UPDATE TimeEntries SET InvoiceId = {{0}} WHERE Id IN ({query.ToQueryString()})";

    context.Database.ExecuteSqlRaw(sql, invoice.Id);
}

这种方法的主要缺点是你的代码中会出现原始SQL。然而,我不知道有什么合理的方法来避免当前Entity Framework Core的能力 - 你只能接受这个警告,或者采用其他答案中的警告,例如:
  • 引入依赖于另一个库,如Entity Framework PlusELinq
  • 使用DbContext.SaveChanges()将涉及执行多个SQL查询,每次检索和更新一条记录,而不是批量更新。

3

EF 7支持批量更新:

context
.TimeEntries
.Where(te => timeEntryIds.Contains(te.Id))
.ExecuteUpdate(s => s.SetProperty(
    i => te.InvoiceId,
    i => invoice.Id));

此外,还有一个异步版本的ExecuteUpdate方法,即ExecuteUpdateAsync。


2
在实体框架核心中,你可以使用update range方法进行操作。你可以在这里看到一些样例用法。
using (var context = new YourContext())
{
     context.UpdateRange(yourModifiedEntities);

     // or the followings are also valid
     //context.UpdateRange(yourModifiedEntity1, yourModifiedEntity2, yourModifiedEntity3);
    //context.YourEntity.UpdateRange(yourModifiedEntities);
    //context.YourEntity.UpdateRange(yourModifiedEntity1, yourModifiedEntity2,yourModifiedEntity3);

    context.SaveChanges();
  }

请注意,虽然EF通过对每个实体执行UPDATE语句来提高性能,但它不会像@Dhanuka777上面的示例一样运行单个UPDATE语句以一次更新所有实体。后者要快得多。 - Sandor Drieënhuizen
@SandorDrieënhuizen,我不明白每个实体执行一条UPDATE语句比单个UPDATE更快的原因。所以你是说运行10000次更新比运行一次更快吗?得了吧。 - schlingel
1
@schlingel:尽管您只调用了单个UpdateRange命令,但EF在幕后为每个实体生成一个SQL UPDATE语句。因此,您仍然有效地执行了许多单独的更新查询,这比单个组合的UPDATE查询要慢得多。 - Sandor Drieënhuizen

1
在EF Core 7中,使用ExecuteUpdate()进行批量更新操作。详情请参考新特性
var multipleRows = TableA.Where(t=>t.Id < 99);

multipleRows.ExecuteUpdate(t=> 
    t.SetProperty(
        r => r.Salary,
        r => r.Salary * 2));
    
//SQL already sent to database, do not run below
//SaveChanges(); 

EF 生成的 SQL

UPDATE [t]
SET [t].[Salary] = [t].[Salary] * 2
FROM [TableA] AS [t]
WHERE [t].[ID] < 99

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接