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

如何使用EF Core删除多层嵌套表关联集合并更新数据

问题说明

开发多表关联结构的应用时,需要删除嵌套子表的关联集合并替换为新集合,原有实现存在逻辑漏洞与性能问题,以下是可直接落地的最优方案。


原有代码的核心问题
  • 缺失必要关联预加载:Include/ThenInclude 链只加载到 EstimateCostLineItem.TaxEntity,没有加载目标集合 EstimateCostLineItem.AdditionalExpenses,遍历逻辑根本取不到要删除的关联数据;直接给导航属性赋新空列表的写法,会让 EF Core 丢失对原有实体的追踪,要么旧数据残留数据库,要么触发外键约束异常。
  • 存在严重查询逻辑漏洞:查询没有加当前操作记录的主键过滤,FirstOrDefaultAsync 会返回全表第一条未删除的记录,完全不是要更新的目标数据。
  • 冗余加载无关数据:代码中 Include 了 CustomerAddress、TaxEntity、CompanyAddress 等一堆和删除 AdditionalExpenses 无关的关联表,平白增加数据库查询开销。
  • 冗余判断与低效遍历:三层 foreach 中写的 Count() > 0 判断完全多余——空集合遍历不会报错,反而调用 Count() 会触发不必要的集合枚举;三层嵌套循环可读性差。

最优实现方案

根据你代码里全量使用 IsDeleted 标记的逻辑,默认是软删除架构,以下分两种场景给出实现:

场景1:数据量小(单单据下费用条目<1000条),用EF Core变更追踪实现

这种方式逻辑简单,和你原有代码架构兼容性最好:

public async Task AddUpdateProjectCosting(string userId, ProjectCostingDTO model)
{
    // 只加载需要操作的关联层级,去掉无关Include,补上缺失的AdditionalExpenses加载,加主键过滤
    var estimate = await GetAll()
            .Where(x => x.Id == model.Id && !x.IsDeleted)
            .Include(x => x.EstimateDetails.Where(d => !d.IsDeleted))
                .ThenInclude(x => x.EstimateDetailsSections.Where(s => !s.IsDeleted))
                .ThenInclude(x => x.EstimateCostLineItems.Where(c => !c.IsDeleted))
                .ThenInclude(x => x.AdditionalExpenses.Where(ae => !ae.IsDeleted))
            .FirstOrDefaultAsync();

    if (estimate == null)
        throw new KeyNotFoundException("指定的项目成本记录不存在");

    // 更新主表字段
    estimate.UpdatedBy = userId;
    estimate.UpdatedDate = DateTime.UtcNow;
    estimate.OverHeadPercentage = model.OverHeadPercent;

    // 用SelectMany打平嵌套集合,一次性拿到所有要删除的费用条目,不用写三层foreach
    var allOldExpenses = estimate.EstimateDetails
        .SelectMany(ed => ed.EstimateDetailsSections)
        .SelectMany(eds => eds.EstimateCostLineItems)
        .SelectMany(cl => cl.AdditionalExpenses)
        .ToList();

    // 软删除旧条目
    foreach (var expense in allOldExpenses)
    {
        expense.IsDeleted = true;
        expense.UpdatedBy = userId;
        expense.UpdatedDate = DateTime.UtcNow;
    }

    // 追加新的费用条目,注意不要重新new列表赋值,在原有集合上调用AddRange即可
    // 此处根据自己的DTO映射逻辑写入新的AdditionalExpense实体即可,示例:
    // var allCostLines = estimate.EstimateDetails
    //     .SelectMany(ed => ed.EstimateDetailsSections)
    //     .SelectMany(eds => eds.EstimateCostLineItems)
    //     .ToList();
    // foreach (var line in allCostLines)
    // {
    //     var newExpenses = model.ExpenseDtos
    //         .Where(dto => dto.CostLineId == line.Id)
    //         .Select(dto => new AdditionalExpense
    //         {
    //             // 字段映射
    //             Amount = dto.Amount,
    //             Name = dto.Name,
    //             CreatedBy = userId,
    //             CreatedDate = DateTime.UtcNow
    //         });
    //     line.AdditionalExpenses.AddRange(newExpenses);
    // }

    await Change(estimate);
}

场景2:数据量大,用EF Core批量操作实现(性能高10~100倍)

如果单单据下关联数据超过千条,不需要把所有实体加载到内存,直接用EF Core 7+支持的批量操作实现,软删除、硬删除都支持:

public async Task AddUpdateProjectCosting(string userId, ProjectCostingDTO model)
{
    // 第一步:只查需要的成本行ID,不加载全量实体
    var costLineIds = await GetAll()
        .Where(x => x.Id == model.Id && !x.IsDeleted)
        .SelectMany(x => x.EstimateDetails.Where(d => !d.IsDeleted))
        .SelectMany(x => x.EstimateDetailsSections.Where(s => !s.IsDeleted))
        .SelectMany(x => x.EstimateCostLineItems.Where(c => !c.IsDeleted))
        .Select(x => x.Id)
        .ToListAsync();

    // 批量软删除旧的费用条目,生成单条UPDATE SQL,性能极高
    await _context.Set<AdditionalExpense>()
        .Where(ae => costLineIds.Contains(ae.CostLineId) && !ae.IsDeleted)
        .ExecuteUpdateAsync(s => s
            .SetProperty(p => p.IsDeleted, true)
            .SetProperty(p => p.UpdatedBy, userId)
            .SetProperty(p => p.UpdatedDate, DateTime.UtcNow));

    // 如果是硬删除,替换成ExecuteDeleteAsync即可
    // await _context.Set<AdditionalExpense>()
    //     .Where(ae => costLineIds.Contains(ae.CostLineId) && !ae.IsDeleted)
    //     .ExecuteDeleteAsync();

    // 主表更新、新条目追加逻辑和场景1一致即可
    var estimate = await GetAll().FirstOrDefaultAsync(x => x.Id == model.Id && !x.IsDeleted);
    estimate.UpdatedBy = userId;
    estimate.UpdatedDate = DateTime.UtcNow;
    estimate.OverHeadPercentage = model.OverHeadPercent;
    // 此处追加新的AdditionalExpenses逻辑...

    await Change(estimate);
}

关键注意点
  • 永远不要直接给EF Core的集合导航属性赋值新的List实例,EF Core的变更追踪依赖导航属性的原始集合引用,直接替换会导致追踪失效,正确做法是在原有集合上做Remove、AddRange操作。
  • 只Include当前操作需要的关联表,不需要修改的关联数据不要预加载,能大幅降低数据库IO开销。
  • 只要是超过百条数据的批量更新/删除,优先用ExecuteUpdateAsync/ExecuteDeleteAsync,避免全量加载实体到内存造成的性能瓶颈。

内容的提问来源于stack exchange,提问作者Muhammad Kamran

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 13:09:16