大数据量下批量插入与更新的正确实现方式是什么?
现有代码问题修复
首先你当前代码存在一个致命逻辑错误:CreateOrUpdateDbElement 方法中的判断条件写反了,你现在判断的是if(element is not null),这会导致只要入参element不为空,无论是否从数据库查到对应实体,都会走更新分支,entity为空时直接抛出空引用异常,需要修改为:
// 原错误写法 // if(element is not null) // 修正后 if(entity is not null)
关于DetectChanges相关的疑问
你说的在循环外统一调用DetectChanges,全程关闭AutoDetectChanges的方案是完全安全的,且性能远高于当前实现。
你当前代码来回开关AutoDetectChangesEnabled的操作完全没有必要,手动设置实体Entry的State本身就不依赖自动变化检测。优化方案如下:
- 在
Run方法最开头就将dbContext.Configuration.AutoDetectChangesEnabled设为false - 所有实体的新增、修改操作全部完成后,统一调用一次
dbContext.ChangeTracker.DetectChanges() - 执行
SaveChanges后,再将AutoDetectChangesEnabled恢复为默认值(如果后续还要用这个Context的话)
只要你所有对实体属性的修改都在调用DetectChanges之前完成,这个操作就不存在任何安全问题,且你邮件逻辑用的是独立Context,完全不会干扰当前Context的状态。
另外你当前try块里的执行顺序有误:你先调用了DetectChanges,再执行AddRange添加新增实体,新增实体的变化不会被这次检测覆盖,需要调整顺序为先AddRange,再调用DetectChanges。
关于新增/更新实体是否合并处理的疑问
不需要将待更新实体和新增实体合并到同一个列表,原因如下:
- 待更新的实体已经被当前Context跟踪,你修改属性后,
DetectChanges会自动将其标记为Modified状态,不需要额外添加到列表 - 已经被跟踪的实体如果调用
Add/AddRange,会被强制标记为Added状态,执行插入逻辑而非更新,反而会报错
你当前单独维护新增实体列表的逻辑是合理的,唯一可以优化的点是:你用HashSet存储查找用的数据库实体,却用FirstOrDefault遍历查找,完全浪费了HashSet的O(1)查找特性,建议替换为Dictionary<主键类型, DbElement>存储数据库实体,查找性能会有数倍到数十倍的提升,数据量越大提升越明显。
额外优化建议
如果你不需要全字段更新,不要手动设置Entry.State = EntityState.Modified,这个操作会让EF生成更新所有字段的SQL,只修改属性让DetectChanges自动识别变化,会生成只更新变化字段的更高效SQL,性能差别不大但数据库执行压力更小。
内容的提问来源于stack exchange,提问作者codlix

