You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
}

问题分析

  1. Update方法的误用:DbContext.Update会将整个实体标记为Modified,EF Core会生成包含所有属性的UPDATE语句,WHERE子句会对比实体的原始值与数据库当前值。如果映射后的User实体缺失某些数据库属性值(或值不一致),会被判定为并发冲突,因为UPDATE语句影响行数为0。
  2. 脱离跟踪的实体:通过Mapper映射得到的User实体未被DbContext跟踪,Update方法强制将其状态设为Modified,但EF Core无法获取该实体的原始数据库值,只能用映射后的当前值作为原始值对比,一旦数据库中用户数据有任何差异(哪怕是映射遗漏),就会触发异常。
  3. 关联实体状态未正确处理:新创建的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 13:34:53