如何使用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
相关产品推荐
相关产品推荐

