You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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表

IdPart Number
111A789
222A789

ShipmentRow表

IdPart NumberOrderRowId
333A789null

当前实现是在循环内每次调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 16:54:52