EF Core多对多关联实体更新遇DbUpdateConcurrencyException求助
DbUpdateConcurrencyException在EF Core多对多+一对多关联中的问题排查与解决
业务规则
- 用户可加入多个房间,房间可拥有多个参与者(多对多关联);
- 用户可拥有多个房间,但一个房间最多仅能有一位所有者(一对多关联)。
实体定义
public sealed class User { public Guid UserId { get; set; } public required string FirstName { get; set; } public required string LastName { get; set; } public required string Pseudo { get; set; } public int Age { get; set; } public List<Room> Rooms { get; } = []; public List<UserRoom> UserRooms { get; } = []; public List<Room> OwnedRooms { get; } = []; } public sealed class Room { public Guid RoomId { get; set; } public required string Name { get; set; } public bool IsActive { get; set; } public Guid? OwnerId { get; set; } public User? Owner { get; set; } public List<User> Participants { get; } = []; public List<UserRoom> UserRooms { get; } = []; } public sealed class UserRoom { public User User { get; set; } = null!; public Room Room { get; set; } = null!; public DateTime CreatedAt { get; set; } }
实体配置代码
internal class UserEntityConfiguration : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { builder.ToTable(nameof(User)); builder.HasKey(x => x.UserId); builder.Property(x => x.FirstName) .HasMaxLength(30) .IsRequired(); builder.Property(x => x.LastName) .HasMaxLength(30) .IsRequired(); builder.Property(x => x.Pseudo) .HasMaxLength(30) .IsRequired(); builder.HasMany(x => x.Rooms) .WithMany(x => x.Participants) .UsingEntity<UserRoom>( builder => builder.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()")); } } internal class RoomEntityConfiguration : IEntityTypeConfiguration<Room> { public void Configure(EntityTypeBuilder<Room> builder) { builder.ToTable(nameof(Room)); builder.HasKey(x => x.RoomId); builder.Property(x => x.Name) .HasMaxLength(50) .IsRequired(); builder.HasOne(e => e.Owner) .WithMany(e => e.OwnedRooms) .HasForeignKey(e => e.OwnerId) .OnDelete(DeleteBehavior.NoAction); } }
问题场景
当已持久化的用户创建房间时,触发DbUpdateConcurrencyException,相关代码如下:
用户创建房间方法
public Guid CreateRoom(string name, bool isActive = true, List<User>? participants = null) { var newRoom = Room.Create(name, this, isActive: isActive, participants: participants); OwnedRooms.Add(newRoom); return newRoom.Id; }
数据库保存代码
Guid newRoomId = user.CreateRoom(name, isActive, participants); using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken); try { DbContext.Users.Update(Mapper.Map<Infrastructure.Entities.User>(user)); await DbContext.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); } catch (Exception) { await transaction.RollbackAsync(cancellationToken); }
问题分析
- Update方法的误用:
DbContext.Update会将整个实体标记为Modified,EF Core会生成包含所有属性的UPDATE语句,WHERE子句会对比实体的原始值与数据库当前值。如果映射后的User实体缺失某些数据库属性值(或值不一致),会被判定为并发冲突,因为UPDATE语句影响行数为0。 - 脱离跟踪的实体:通过Mapper映射得到的User实体未被DbContext跟踪,Update方法强制将其状态设为Modified,但EF Core无法获取该实体的原始数据库值,只能用映射后的当前值作为原始值对比,一旦数据库中用户数据有任何差异(哪怕是映射遗漏),就会触发异常。
- 关联实体状态未正确处理:新创建的Room及其关联的UserRoom可能未被正确标记为Added,导致EF Core错误地尝试更新这些实体而非插入,引发冲突。
解决方案建议
方案1:使用跟踪实体更新关联集合
避免直接映射和使用Update,先从数据库查询出被跟踪的User实体,再更新其关联集合:
var dbUser = await DbContext.Users .Include(u => u.OwnedRooms) .FirstAsync(u => u.UserId == user.UserId, cancellationToken); var newRoom = Room.Create(name, dbUser, isActive, participants); dbUser.OwnedRooms.Add(newRoom); await DbContext.SaveChangesAsync(cancellationToken);
这种方式下EF Core会自动跟踪所有实体状态,仅生成必要的INSERT/UPDATE语句,避免不必要的并发检查。
方案2:手动控制实体状态
如果必须使用映射后的实体,手动设置状态而非使用Update:
var mappedUser = Mapper.Map<Infrastructure.Entities.User>(user); var entry = DbContext.Entry(mappedUser); entry.State = EntityState.Unchanged; // 标记用户实体为未更改,避免全量更新 entry.Collection(u => u.OwnedRooms).IsModified = true; // 仅标记OwnedRooms集合为修改 // 手动添加新创建的Room实体 foreach (var room in mappedUser.OwnedRooms.Where(r => !DbContext.Rooms.Local.Any(dbR => dbR.RoomId == r.RoomId))) { DbContext.Rooms.Add(room); } await DbContext.SaveChangesAsync(cancellationToken);
其他检查点
- 确认实体是否配置了并发令牌(如RowVersion),如果有,确保映射时正确传递了该字段的值,否则会触发令牌不匹配的并发异常。
- 检查
Room.Create方法,确保OwnerId被正确设置为当前用户的UserId,避免关联关系错误导致的更新逻辑异常。
内容的提问来源于stack exchange,提问作者Vic7or
相关产品推荐
相关产品推荐

