EF Core非全量加载实体更新部分列时抛出DbUpdateConcurrencyException
问题分析与解决方案
首先直接给结论:你这种投影到新LearningDocument实例的方式本身是可行的,但你遗漏了关键的并发令牌列VersionStamp,这正是导致DbUpdateConcurrencyException的原因。
为什么会抛出并发异常?
你的实体类里有VersionStamp(timestamp类型),EF Core会自动把这种类型的列识别为并发令牌——在更新时,EF会用这个值来验证数据是否被其他进程修改过。但你在投影的时候只选择了LearningDocumentId、IsDone和ModifiedDateTime,没有包含VersionStamp。当你Attach这个新实例并执行更新时:
- EF生成的UPDATE语句会尝试匹配
LearningDocumentId,但同时会检查VersionStamp是否与数据库中的值一致; - 由于你的实例里
VersionStamp是默认值(空byte数组),和数据库中实际的时间戳不匹配,导致数据库中没有符合条件的行被更新,于是抛出“预期影响1行,实际影响0行”的异常。
修复方案
方案1:在投影中包含并发令牌列
只需要修改Select部分,把VersionStamp也包含进来,这样EF就能正确进行并发检查,定位到要更新的行:
var dbContext = GetDBContext(); var documents = await dbContext.LearningDocuments .Where(x => x.LearningRequestID == 1234) .Select(x => new LearningDocument() { LearningDocumentId = x.LearningDocumentId, IsDone = x.IsDone, ModifiedDateTime = x.ModifiedDateTime, VersionStamp = x.VersionStamp // 新增:带上并发令牌列 }) .ToListAsync() .ConfigureAwait(false); documents.ForEach(d => { d.IsDone = true; d.ModifiedDateTime = DateTime.UtcNow; dbContext.LearningDocuments.Attach(d); dbContext.Entry(d).Property(x => x.IsDone).IsModified = true; dbContext.Entry(d).Property(x => x.ModifiedDateTime).IsModified = true; }); await dbContext.SaveChangesAsync().ConfigureAwait(false);
方案2:使用EF Core的ExecuteUpdate(推荐,EF Core 7+支持)
如果你只是要批量更新特定列,完全不需要把实体加载到内存——EF Core 7及以上版本提供的ExecuteUpdate可以直接在数据库层面执行批量更新,性能更高,代码也更简洁:
var dbContext = GetDBContext(); await dbContext.LearningDocuments .Where(x => x.LearningRequestID == 1234) .ExecuteUpdateAsync(s => s .SetProperty(x => x.IsDone, true) .SetProperty(x => x.ModifiedDateTime, DateTime.UtcNow)) .ConfigureAwait(false);
这种方式不会加载任何实体到内存,直接生成并执行UPDATE SQL语句,同时如果需要并发检查,也可以在Where条件中加入VersionStamp的判断(比如匹配特定版本)。
总结
投影到新实体实例来更新部分列是可行的,但必须确保包含主键和并发令牌(如果有的话)这两类关键属性,否则EF无法正确定位和验证数据库中的行。而对于批量更新场景,ExecuteUpdate是更优的选择,既满足你“不完整加载实体到内存”的需求,又能提升性能。
内容的提问来源于stack exchange,提问作者LP13
相关产品推荐
相关产品推荐

