EF Core 7中ChangeStatusType重复跟踪错误排查求助
EF Core 7批量创建Change时跟踪冲突问题
错误信息
实体类型'ChangeStatusType'的实例无法被跟踪,因为已有一个具有相同{'ChangeStatusTypeId'}键值的实例被跟踪。附加现有实体时,请确保每个键值仅附加一个实体实例。
实体关系
ChangeRequest包含多个Changes- 每个
Change可包含多个ChangeStatus - 每个
ChangeStatus对应一个ChangeStatusType
问题场景
创建单个Change及其关联的ChangeStatus、ChangeStatusType时正常,但批量创建多个Change时触发上述跟踪冲突错误。
原始保存代码
foreach (var change in addedChanges) { context.Entry(change).State = EntityState.Added; foreach (var changeStatus in change.ChangeStatuses) { context.ChangeTracker.TrackGraph(changeStatus, x => { x.Entry.State = EntityState.Added; }); context.Attach(changeStatus.StatusTypes); } } context.AddRange(addedChanges); var result = await context.SaveChangesAsync().ConfigureAwait(false);
实体类定义
ChangeStatus
public class ChangeStatus { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. /// <summary> /// Constructor used by EF. /// </summary> public ChangeStatus() { } #pragma warning restore CS8618 public int? ChangeStatusId { get; set; } [ForeignKey(nameof(ChangeDetail.ChangeDetailId))] public int? ChangeDetailId { get; set; } [Required] [Column(TypeName = "nvarchar(150)")] public string SubmitUser { get; set; } = string.Empty; [Required] public DateTime StatusDate { get; set; } [ForeignKey(nameof(ChangeStatusType.ChangeStatusTypeId))] public int ChangeStatusTypeId { get; set; } public virtual ChangeStatusType StatusTypes { get; set; } public virtual List<ChangeDetail> Changes { get; set; } }
ChangeStatusType
public class ChangeStatusType { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. /// <summary> /// Constructor used by EF. /// </summary> public ChangeStatusType() { } #pragma warning restore CS8618 public int ChangeStatusTypeId { get; set; } [Required] [StringLength(50)] public string StatusTypeName { get; set; } = string.Empty; }
更新后的尝试代码(问题仍存在)
foreach (var change in addedChanges) { context.ChangeDetail.Add(change); // Change.ChangeStatuses会被自动标记为Added,但我们不希望引用实体被标记为Add foreach (var changeStatus in change.ChangeStatuses) { //context.ChangeTracker.TrackGraph(changeStatus, x => { x.Entry.State = EntityState.Added; }); var existingStatusType = context.ChangeStatusType.Local .FirstOrDefault(x => x.ChangeStatusTypeId == changeStatus.StatusTypes.ChangeStatusTypeId); if (existingStatusType != null) changeStatus.StatusTypes = existingStatusType; else context.Attach(changeStatus.StatusTypes); } foreach (var entry in context.ChangeTracker.Entries()) { var entityName = entry.Entity.GetType().Name; var state = entry.State.ToString(); Debug.WriteLine(state + " " + entityName); } }
解决方案
核心原因
批量处理时,不同ChangeStatus的StatusTypes可能是ID相同但实例不同的对象,EF跟踪器会将它们视为独立实体,尝试附加时触发主键冲突。
方案1:仅使用外键ID(推荐)
对于已存在的ChangeStatusType,无需赋值StatusTypes实体,只需确保ChangeStatus.ChangeStatusTypeId正确设置,EF会自动通过外键关联数据库记录,避免跟踪冲突:
foreach (var change in addedChanges) { foreach (var changeStatus in change.ChangeStatuses) { // 清除实体引用,仅保留外键ID(需确保ChangeStatusTypeId已正确赋值) changeStatus.StatusTypes = null; } } context.AddRange(addedChanges); var result = await context.SaveChangesAsync().ConfigureAwait(false);
方案2:复用上下文跟踪的实体实例
如果必须保留StatusTypes实体引用,使用FindAsync从上下文(优先本地缓存,再查数据库)获取实例,确保同一个ID只有一个实例被跟踪:
foreach (var change in addedChanges) { foreach (var changeStatus in change.ChangeStatuses) { // 从上下文获取已存在的ChangeStatusType实例 var statusType = await context.ChangeStatusType .FindAsync(changeStatus.ChangeStatusTypeId) .ConfigureAwait(false); if (statusType != null) { // 替换为上下文跟踪的实例 changeStatus.StatusTypes = statusType; } else { // 若数据库也不存在(你已确认TypeId存在,此分支可忽略),附加并标记为Unchanged context.Attach(changeStatus.StatusTypes).State = EntityState.Unchanged; } } } context.AddRange(addedChanges); var result = await context.SaveChangesAsync().ConfigureAwait(false);
方案3:临时禁用自动跟踪
如果上述方案不适用,可在批量操作时禁用跟踪,完成后恢复:
using (var transaction = await context.Database.BeginTransactionAsync().ConfigureAwait(false)) { try { // 禁用自动跟踪 context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; foreach (var change in addedChanges) { foreach (var changeStatus in change.ChangeStatuses) { context.Attach(changeStatus.StatusTypes).State = EntityState.Unchanged; } context.Add(change); } await context.SaveChangesAsync().ConfigureAwait(false); await transaction.CommitAsync().ConfigureAwait(false); } catch { await transaction.RollbackAsync().ConfigureAwait(false); throw; } finally { // 恢复跟踪行为 context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; } }
内容的提问来源于stack exchange,提问作者USMC6072
相关产品推荐
相关产品推荐

