EF Core 6如何避免循环内调用SaveChanges实现批量保存
批量关联OrderRow与ShipmentRow时避免循环内调用SaveChanges的优化方案
问题背景
我们有两个关联实体:
public class OrderRow { public int Id { get; set; } public string PartNumber { get; set; } public List<ShipmentRow> Rows { get; set; } } public class ShipmentRow { public int Id { get; set; } public string PartNumber { get; set; } public int? OrderRowId { get; set; } public OrderRow? OrderRow { get; set; } }
现有数据库数据:
OrderRows表
| Id | Part Number |
|---|---|
| 111 | A789 |
| 222 | A789 |
ShipmentRow表
| Id | Part Number | OrderRowId |
|---|---|---|
| 333 | A789 | null |
当前实现是在循环内每次调用SaveChanges,但性能低下;如果把SaveChanges移到循环外,会因为数据库查询无法感知上下文未保存的变更,导致同一个ShipmentRow被重复关联到多个OrderRow。尝试过的内存检查方案和全量加载内存方案,要么逻辑冗余,要么在大数据量下内存占用过高。
优化方案
方案1:利用EF上下文本地追踪集合过滤(纯EF实现,中小数据量适用)
通过EF的Local集合获取已经被上下文追踪的ShipmentRow,在数据库查询时排除这些已关联的记录,避免重复获取:
var orderRows = dbContext.OrderRow.ToList(); foreach (var orderRow in orderRows) { // 查询未关联且未被上下文追踪的ShipmentRow var sRows = dbContext.ShipmentRow .Where(x => x.PartNumber == orderRow.PartNumber && x.OrderRowId == null && !dbContext.ShipmentRow.Local.Any(l => l.Id == x.Id)) .ToList(); foreach (var s in sRows) { orderRow.Rows.Add(s); s.OrderRowId = orderRow.Id; // 手动设置外键,确保追踪状态正确 } } dbContext.SaveChanges();
优势:无需全量加载所有ShipmentRow,仅加载需要关联的部分,内存占用可控,纯EF代码无需原生SQL。
方案2:数据库层面批量处理(大数据量最优)
直接通过原生SQL在数据库端完成关联操作,完全避免内存加载实体,性能最高:
// SQL Server示例,其他数据库可调整语法 dbContext.Database.ExecuteSqlRaw(@" WITH RankedShipments AS ( SELECT sr.Id, sr.PartNumber, orr.Id AS OrderRowId, -- 按OrderRowId排序,确保每个ShipmentRow只关联到第一个匹配的OrderRow(和原逻辑一致) ROW_NUMBER() OVER (PARTITION BY sr.Id ORDER BY orr.Id) AS RowNum FROM ShipmentRow sr JOIN OrderRow orr ON sr.PartNumber = orr.PartNumber WHERE sr.OrderRowId IS NULL ) UPDATE ShipmentRow SET OrderRowId = rs.OrderRowId FROM ShipmentRow sr JOIN RankedShipments rs ON sr.Id = rs.Id WHERE rs.RowNum = 1; ");
优势:大数据量下性能碾压EF实体操作,无需加载任何数据到内存,直接在数据库完成关联。
方案3:分批处理+已处理ID追踪(中等偏大数据量适用)
通过分批加载OrderRow,并用HashSet记录已处理的ShipmentRow ID,查询时排除这些ID,平衡内存占用和代码复杂度:
var processedShipmentIds = new HashSet<int>(); int batchSize = 50; // 根据实际内存情况调整批次大小 // 分批获取OrderRow(此处用MoreLinq的Batch方法,也可自行实现分批逻辑) foreach (var orderBatch in dbContext.OrderRow.ToList().Batch(batchSize)) { foreach (var orderRow in orderBatch) { var sRows = dbContext.ShipmentRow .Where(x => x.PartNumber == orderRow.PartNumber && x.OrderRowId == null && !processedShipmentIds.Contains(x.Id)) .ToList(); foreach (var s in sRows) { orderRow.Rows.Add(s); s.OrderRowId = orderRow.Id; processedShipmentIds.Add(s.Id); } } } dbContext.SaveChanges();
优势:通过分批减少单次内存加载量,HashSet查询效率极高,避免重复关联,适合数据量较大但不想写原生SQL的场景。
方案选择建议
- 中小数据量:优先方案1,纯EF实现简单易维护。
- 大数据量:优先方案2,性能最优,资源消耗最少。
- 中等偏大且需避免原生SQL:方案3,平衡内存和代码复杂度。
内容的提问来源于stack exchange,提问作者Zenigata
相关产品推荐
相关产品推荐

