如何使用实体框架更新批量数据?

Posted

技术标签:

【中文标题】如何使用实体框架更新批量数据?【英文标题】:How to update bulk data using Entity Framework? 【发布时间】:2022-01-07 11:59:00 【问题描述】:

我的 Oracle 数据库中有一个名为 profile-list 的表。该表有 30K 行。所以用户想用 Excel 下载这些数据并进行更改。更改完成后,他们希望使用表单上传 Excel 并更新表格。

如何使用实体框架来做到这一点?

var bulkData = ReadFromExcel(filePath);

using(var context = new DataCtx())

    var dbData = context.ProfileData.ToList();

    foreach(var item in dbData)
    
        if (bulkData.Any(x => x.id = item.id)
             // update
        else
             // insert
    

但这需要很长时间。我怎样才能更快地做到这一点?

【问题讨论】:

【参考方案1】:

你没有。或者 - 你不使用 EF。 Ef 是一个对象/关系映射器,它的批量操作能力非常有限。

如果这是一个“一个”(即一个特定的用例,不是针对每个表,但也是一个更频繁发生的)操作,我可能会定义一个表值参数,并使用一个作为输入的 SP如果我需要性能,可以批量推送更新。定义一个映射器来映射它是非常简单的(几个小时的工作)。

对于更多数据,事情会变得更加复杂。但随后我们谈论定期进行 50k+ 更新,并行执行许多线程。远高于您的用例。

但从字面上看,Ef 有一个非常具体的用例。它不处理批量操作。不过,这里有一个提示——即使这样,它也远非无用。处理数据需要 80% 的场景中的 95% 的工作,包括所有极其灵活的查询。这使您有时间为其余部分寻找和实施优雅的解决方案。而您的案例正是 EF 无法有效处理的案例之一。

现在,如果您想加快您的代码速度,其中存在一个根本性错误,如果仅进行少量更改,可能真的会大大缩短您的处理时间:

var dbData = context.ProfileData.ToList();

这是一个列表。然后你使用

if(bulkData.Any(x=>x.id = item.id)

在循环中查找每一行。这非常慢 - 平均而言,每个 ID 都必须经过一半的列表。

因此,不要使用 ToList,而是使用以 id 作为键的 ToDictionary。在循环中,您可以更有效地提取单个项目。很多。因为查找不会扫描每个项目的一半列表。

ToDictionary(x => x.id)

然后最后做一个 SaveChanges - 但这将是你最终可能需要很长时间的地方。现在,如果没有那么多更改,这可能会更有效率,并且已经让您进入“好的,可用”的领域。它必须跟踪更改(这需要时间),但至少您不会对列表进行那么慢的扫描。

【讨论】:

【参考方案2】:

EF 不太适合批量操作,本质上,批量操作不是您通常希望完成的操作,例如网络应用程序上的操作或用户可能“启动”的任何操作,因为这需要一段时间,肯定会占用服务器上的资源,加上锁行等导致潜在的死锁。

不过,如果这些风险是可以接受的,并且此操作很少由授权的、负责任的用户执行,那么您可以使用 EF 实施某些操作。

这种方法的最大问题是一次加载所有内容。导出和重新导入数据并不是提供数据编辑的理想方法,因为您无法针对用户实际更改的数据进行优化。如果他们编辑了 30k 行中的 10 行,那么您仍在处理 30k 行。让他们能够通过应用程序查询数据和进行更改比导出和重新导入要好得多。

在一个 DbContext 实例中处理 30k 行,无论您最后使用一个 SaveChanges(),还是在每次更新行时调用 SaveChanges(),都会显着拖累性能。而不是加载整个表格:

    将 Excel 数据读入内存。 一次获取 100 行数据。 更新行,确保验证每一行。 该批次上的 SaveChages()。 处理异常。 在批次之间刷新 DBContext 实例。

所以更像是:

var bulkData = ReadFromExcel(filePath); // (#1)

for(int count = 0; count < bulkData.Count; count += 100)

    var sourceData = bulkData.Skip(count).Take(100); // (#2)
    using (var context = new DataCtx())
    
        try
        
            var ids = sourceData.Select(x => x.id).ToList(); // Get IDs to load.
            var data = context.ProfileData.Where(x => ids.Contains(x.id)).ToList();
            foreach(var source in sourceData)
            
                var dest = data.SingleOrDefault(x => x.id == source.id);
                if (dest == null)
                    // add.
                else
                    // validate update dest with details from source (#3)
             
             context.SaveChanges(); // will commit this batch of 100. (#4)
        
        catch(Exception e)
         // What should happen if something slips through validation? these ~100 items won't be saved. (#5)
        
     // Context is disposed with each batch of 100 (#6)

 // Loop to next 100.

您可能需要考虑一次将 Excel 数据的子集加载到内存中,例如一次加载 10k 行或更少,以避免内存占用过大。这取决于实际发生的时间/频率。

批处理之类的最大风险是允许用户触发它。如何阻止 100 个用户同时上传数据副本?或在其他用户忙于读取/更新数据并遇到此行程行锁和死锁时上传数据?至少如果此操作可以由用户触发,则上传 excel 文档以进行更新的行为应该上传文件,排队请求处理,并返回一个令牌,用户可以在轮询请求中再次提交以检查他们的上传状态。单个后台工作人员可以确保一次只处理一个上传,甚至可以让它排队等待一天中的预定时间进行更新,而不会影响用户。

【讨论】:

【参考方案3】:

免责声明:我是Entity Framework Extensions的所有者

这个库不是免费的,但可以通过 BulkMerge 方法完全满足您的需求:

var bulkData = ReadFromExcel(filePath);

using(var context = new DataCtx())

    var dbData = context.ProfileData.ToList();

    // update existing, insert new entities
    context.BulkMerge(dbData);

该库支持 Oracle、SQL Server 等。

正如其他人回答的那样,Entity Framework 不支持批量操作,因此您需要创建代码来处理它,或者使用支持它的第三方库。

【讨论】:

以上是关于如何使用实体框架更新批量数据?的主要内容,如果未能解决你的问题,请参考以下文章

实体框架的大批量更新比我自己批量更新慢得多

如何使用对象上下文在实体框架中使用批量插入?

如何使用实体框架从 DataGrid 更新数据库

如何首先使用实体​​框架代码更新一行?

如何使用实体框架更新数据库中的记录时覆盖模型验证

使用 System.Transaction 如何更新实体框架中的多行