EF Core添加实体至列表时触发DbUpdateConcurrencyException问题
解决EF Core中添加子Package触发DbUpdateConcurrencyException的问题
问题分析与解答
1. 为什么添加子Package会触发并发异常,而修改属性和更新Components不会?
核心差异在于EF Core对父实体existingPackage的并发检查触发逻辑:
- 直接修改
existingPackage的属性时,你手动更新了并发令牌字段(比如ModifiedDate),EF Core会生成包含旧令牌值的UPDATE语句,只要数据库中该实体的令牌值未被其他操作修改,就能成功执行。 - 向
existingPackage.Packages添加子Package时,EF Core默认不会自动更新父实体的并发令牌,但如果你的数据库(如触发器)或模型配置中,子实体关联变更会触发父实体令牌更新,此时EF Core会用加载时的旧令牌值去验证父实体状态,若数据库中父实体令牌已被修改,UPDATE语句受影响行数为0,触发并发异常。 - 而
widget.Components的修改是直接更新现有实体属性,未触发关联实体新增,EF Core仅对Component实体执行UPDATE,并发检查逻辑正常通过。
2. 正确添加子Package的解决方案
以下是几种可行的解决方式:
方式一:手动更新父实体并发令牌
添加子Package后,手动更新父实体的并发令牌字段,让EF Core跟踪父实体变更,确保并发检查通过:
existingPackage.Packages.AddRange(package.Packages); // 手动更新并发令牌字段,如ModifiedDate existingPackage.ModifiedDate = DateTime.Now;
方式二:使用数据库自动生成的RowVersion作为并发令牌
将RowVersion(数据库自动生成的时间戳)配置为并发令牌,EF Core会自动跟踪实体所有变更(包括关联子实体的新增),无需手动维护:
// 在Package的模型配置类中 public void Configure(EntityTypeBuilder<Package> builder) { builder.Property(p => p.RowVersion) .IsRowVersion() .IsConcurrencyToken(); }
方式三:临时禁用父实体并发检查(不推荐,仅适用于低并发场景)
如果确定父实体不会被其他操作并发修改,可以临时禁用其并发检查:
// 临时禁用并发检查 _context.Entry(existingPackage).Property(p => p.ModifiedDate).IsConcurrencyToken = false; existingPackage.Packages.AddRange(package.Packages); // 保存后可恢复并发检查 _context.Entry(existingPackage).Property(p => p.ModifiedDate).IsConcurrencyToken = true;
3. EF Core处理并发问题的实用技巧
- 优先使用RowVersion作为并发令牌:相比手动维护的
ModifiedDate,数据库自动生成的RowVersion能自动跟踪实体所有变更,避免手动更新遗漏。 - 显式加载并发令牌:查询实体时确保包含并发令牌字段,EF Core需要原始值执行并发检查。
- 实现乐观并发重试机制:高并发场景下,捕获
DbUpdateConcurrencyException后重新加载实体并重试更新:
public async Task<bool> UpdateWidgetWithRetry(Guid widgetId, Guid packageId, Package package, int retryCount = 3) { for (int i = 0; i < retryCount; i++) { try { var widget = await _context.Widgets .Include(w => w.Packages) .ThenInclude(p => p.Packages) .Include(w => w.Components) .SingleOrDefaultAsync(w => w.Id == widgetId); var existingPackage = widget.Packages.SingleOrDefault(p => p.Id == packageId); existingPackage.Packages.AddRange(package.Packages); existingPackage.ModifiedDate = DateTime.Now; await _context.SaveChangesAsync(); return true; } catch (DbUpdateConcurrencyException) { // 清空上下文,避免跟踪旧实体 _context.ChangeTracker.Clear(); if (i == retryCount - 1) throw; } } return false; }
- 排查冲突实体:捕获异常后,通过
DbUpdateConcurrencyException.Entries属性查看具体冲突的实体,针对性处理。 - 区分关联变更的并发逻辑:对于关联实体的新增/删除,确保父实体的并发令牌被正确更新,要么手动维护,要么依赖
RowVersion自动处理。
内容的提问来源于stack exchange,提问作者atamata
相关产品推荐
相关产品推荐

