EF Core 7单向多对多对象插入更新的实体跟踪问题
EF Core 7添加Team对象时Position跟踪冲突问题
类结构
public class Team { public int Id { get; set; } public string Name { get; set; } public List<Player> Players { get; set; } public int SportId { get; set; } public Sport Sport { get; set; } } public class Player { public int Id { get; set; } public string Name { get; set; } public List<Position> Positions { get; set; } public int SportId { get; set; } public Sport Sport { get; set; } } public class Position { public int Id { get; set; } public string Name { get; set; } } public class Sport { public int Id { get; set; } public string Name { get; set; } public ICollection<Player> Players { get; set; } = null!; }
DbContext配置
public class AppDbContext : DbContext { public DbSet<Team> Teams { get; set; } public DbSet<Player> Players { get; set; } public DbSet<Position> Positions { get; set; } public DbSet<Sport> Sports { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Data Source=localhost;Initial Catalog=Scrap;User Id=sa;Password=P@ssw0rd;TrustServerCertificate=true;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Player>().HasOne(x => x.Sport).WithMany(x => x.Players).HasForeignKey(x => x.SportId).OnDelete(DeleteBehavior.NoAction); modelBuilder.Entity<Team>().HasMany(x => x.Players).WithMany(); modelBuilder.Entity<Player>().HasMany(x => x.Positions).WithMany(); } }
添加方法
using (var context = await _contextFactory.CreateDbContextAsync()) { context.Teams.Add(team); await context.SaveChangesAsync(); }
待添加的JSON数据
{ "id": 0, "name": "Fireball", "players": [ { "id": 8, "name": "jim", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": { "id": 1, "name": "Football - american", "players": [ { "id": 9, "name": "joe", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 12, "name": "blake", "positions": [ { "id": 1, "name": "Qb" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 13, "name": "erik", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null } ] } }, { "id": 9, "name": "joe", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": { "id": 1, "name": "Football - american", "players": [ { "id": 8, "name": "jim", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, null, { "id": 12, "name": "blake", "positions": [ { "id": 1, "name": "Qb" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 13, "name": "erik", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null } ] } }, { "id": 12, "name": "blake", "positions": [ { "id": 1, "name": "Qb" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": { "id": 1, "name": "Football - american", "players": [ { "id": 8, "name": "jim", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 9, "name": "joe", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 13, "name": "erik", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null } ] } }, { "id": 13, "name": "erik", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": { "id": 1, "name": "Football - american", "players": [ { "id": 8, "name": "jim", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 9, "name": "joe", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 12, "name": "blake", "positions": [ { "id": 1, "name": "Qb" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null } ] } } ], "sportId": 1, "sport": { "id": 1, "name": "Football - american", "players": [ { "id": 8, "name": "jim", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 9, "name": "joe", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 12, "name": "blake", "positions": [ { "id": 1, "name": "Qb" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null }, { "id": 13, "name": "erik", "positions": [ { "id": 1, "name": "Qb" }, { "id": 2, "name": "WR" }, { "id": 3, "name": "RB" } ], "sportId": 1, "sport": null } ] } }
报错信息
The instance of entity type 'Position' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked
问题分析与解决方案
问题根源
JSON反序列化后生成了多个相同Id的Position实例,EF Core上下文在跟踪这些实例时,发现同一主键对应的实体被多次创建,触发跟踪冲突。此外,JSON中携带了大量冗余的嵌套关联数据(如Player的Sport、Sport的Players),这些数据不仅无用,还会加重上下文跟踪负担。
解决方案
方案1:清理冗余数据,仅保留必要的主键关联
反序列化后先清理不需要的嵌套对象,只保留Position的Id、Player的Id及SportId,避免EF尝试跟踪多余的实体实例:
// 反序列化得到team对象后执行以下清理 foreach (var player in team.Players) { // 只保留Position的Id,丢弃其他属性 player.Positions = player.Positions.Select(p => new Position { Id = p.Id }).ToList(); // 清除Player的嵌套Sport对象,只保留SportId player.Sport = null; } // 清除Team的嵌套Sport对象,只保留SportId team.Sport = null; using (var context = await _contextFactory.CreateDbContextAsync()) { // 提取所有唯一的Position Id var positionIds = team.Players.SelectMany(p => p.Positions).Select(p => p.Id).Distinct(); // 附加所有涉及的Position,标记为未修改 foreach (var id in positionIds) { context.Positions.Attach(new Position { Id = id }).State = EntityState.Unchanged; } // 附加所有涉及的Player,标记为未修改 foreach (var player in team.Players) { context.Players.Attach(player).State = EntityState.Unchanged; } // 添加Team并保存 context.Teams.Add(team); await context.SaveChangesAsync(); }
方案2:复用上下文跟踪的Position实例
从数据库获取已存在的Position,替换Player中的Position实例,确保每个Id只有一个被跟踪的对象:
using (var context = await _contextFactory.CreateDbContextAsync()) { // 提取所有需要的Position Id var positionIds = team.Players.SelectMany(p => p.Positions).Select(p => p.Id).Distinct(); // 从数据库获取或附加这些Position var existingPositions = await context.Positions.Where(p => positionIds.Contains(p.Id)).ToListAsync(); foreach (var player in team.Players) { // 用上下文跟踪的实例替换Player中的Position player.Positions = existingPositions.Where(p => player.Positions.Select(pp => pp.Id).Contains(p.Id)).ToList(); // 附加Player并标记为未修改 context.Players.Attach(player).State = EntityState.Unchanged; } // 附加Sport并标记为未修改 context.Sports.Attach(new Sport { Id = team.SportId }).State = EntityState.Unchanged; // 清除Team的嵌套Sport对象 team.Sport = null; // 添加Team并保存 context.Teams.Add(team); await context.SaveChangesAsync(); }
方案3:禁用自动跟踪(不推荐,仅临时应急)
如果只需要创建Team与Player、Position的关联,不需要修改现有实体,可以临时禁用上下文的自动跟踪:
using (var context = await _contextFactory.CreateDbContextAsync()) { context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // 清理冗余数据(同方案1) foreach (var player in team.Players) { player.Positions = player.Positions.Select(p => new Position { Id = p.Id }).ToList(); player.Sport = null; } team.Sport = null; context.Teams.Add(team); await context.SaveChangesAsync(); }
内容的提问来源于stack exchange,提问作者CliffDev
相关产品推荐
相关产品推荐

