使用DateTime字段处理Entity Framework并发时遇DbUpdateConcurrencyException问题
这个问题我之前处理过好几次,核心原因就是数据库存储的DateTime带毫秒精度,但EF读取实体时截断了这部分信息,导致更新时用截断后的LastModificationTime和数据库里的完整时间对比,触发并发冲突。
为什么会出现这个情况?
SQL Server的datetime2类型支持最高7位的毫秒精度(比如你数据库里的2020-03-09 10:02:37.4570000),但EF默认映射DateTime类型时,可能没有保留足够的精度,或者你的实体属性没有明确指定对应的数据库列类型,导致读取时丢失了毫秒部分。这样一来,你获取的intervenant.LastModificationTime是2020-03-09T10:02:37(无毫秒),和数据库实际值对比自然不相等,触发DbUpdateConcurrencyException。
解决方案
1. 统一实体与数据库的DateTime精度(最根本的解决办法)
你需要确保实体的LastModificationTime属性和数据库列的精度完全匹配。可以通过两种方式配置:
方式一:使用数据注解
直接在实体属性上添加[Column]特性,指定数据库列类型为datetime2(7)(对应最高精度):
[ConcurrencyCheck] [Column(TypeName = "datetime2(7)")] public DateTime LastModificationTime { get; set; }
方式二:使用Fluent API(推荐,更灵活)
在DbContext的OnModelCreating方法中配置属性的精度:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Intervenant>() .Property(p => p.LastModificationTime) .IsConcurrencyToken() // 等效于[ConcurrencyCheck] .HasColumnType("datetime2(7)") // 指定列类型和精度 .HasPrecision(7); // EF Core 5+可以直接指定精度,无需写ColumnType }
配置完成后,记得生成并执行数据库迁移,确保数据库列类型更新为datetime2(7),这样EF读取和写入时都会保留完整的毫秒精度,对比时就不会出现不一致了。
2. 确保查询获取最新的完整精度值
有时候,EF的一级缓存可能会返回之前读取的截断了毫秒的实体。你可以在查询时跳过缓存,强制从数据库获取最新数据:
// 用Find方法(会绕过一级缓存,直接查数据库) var intervenant = this.IntervenantRepository.Find(intervenantId); // 或者在EF中使用AsNoTracking后再跟踪(适用于EF Core) var intervenant = this.IntervenantRepository.GetAll() .AsNoTracking() .FirstOrDefault(intervenant => intervenant.Id == intervenantId); this.IntervenantRepository.Attach(intervenant); // 重新跟踪实体
3. 处理并发异常的兜底方案
如果偶尔还是会出现并发冲突(比如多人同时修改),可以捕获DbUpdateConcurrencyException,重新读取最新实体,合并修改后再次尝试更新:
try { var intervenant = this.IntervenantRepository.GetAll().FirstOrDefault(intervenant => intervenant.Id == intervenantId); if (IsInscrire) { intervenant.MotifdesinscId = null; intervenant.IsInscrit = true; } this.IntervenantRepository.Update(intervenant); this.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { // 获取数据库中的最新实体 var entry = ex.Entries.Single(); var databaseValues = entry.GetDatabaseValues(); var intervenantDb = databaseValues.ToObject<Intervenant>(); // 合并修改:保留用户的修改,更新LastModificationTime为最新值 intervenantDb.MotifdesinscId = intervenant.MotifdesinscId; intervenantDb.IsInscrit = intervenant.IsInscrit; // 更新实体并重新保存 entry.OriginalValues.SetValues(databaseValues); entry.CurrentValues.SetValues(intervenantDb); this.SaveChanges(); }
总结
优先解决精度匹配的问题(方案1),这是从根源上避免并发异常的方法;方案2和3作为辅助,分别解决缓存问题和处理极端情况下的并发冲突。
内容的提问来源于stack exchange,提问作者ucef

