EF Core中保存DDD聚合根User时Upsert子实体Appointment的实现异常排查
解决EF Core聚合根子实体插入时的乐观并发异常问题
你遇到的乐观并发异常,根源在于直接替换聚合根的子实体集合导致EF Core的变更跟踪误判,尝试删除一个从未被持久化到数据库的子实体。
错误原因详解
测试与仓库代码的交互过程中出现了以下冲突:
- 测试中,你从数据库加载了被上下文跟踪的
ApplicationUserEntity,并给它的Appointments集合添加了一个新的AppointmentEntity(此时这个子实体仅存在于内存,状态为Added)。 - 在仓库的
Update方法中,你直接将persistenceUserEntity.Appointments赋值为新的List<AppointmentEntity>,EF Core会认为原来集合中的所有子实体(包括那个未保存的新实体)都被移除了。 - 调用
Save时,EF Core尝试执行DELETE语句删除那个从未被插入数据库的子实体,自然影响0行,触发乐观并发异常。
修复方案:操作集合内容而非替换整个集合
不要直接替换Appointments集合,而是通过更新现有子实体、添加新子实体、移除不再存在的子实体的方式维护集合,让EF Core的变更跟踪能正确识别每个子实体的状态:
public async Task Update(IApplicationUserWithAppointments domainUserEntity) { ApplicationUserEntity persistenceUserEntity = await FindEntityById(domainUserEntity.Id); var existingAppointmentsById = persistenceUserEntity.Appointments .ToDictionary(appt => appt.Id); var domainAppointmentsById = domainUserEntity.Appointments .ToDictionary(appt => appt.Id); // 更新用户根实体属性 persistenceUserEntity.UserName = domainUserEntity.UserName; persistenceUserEntity.Password = domainUserEntity.Password; persistenceUserEntity.FirstName = domainUserEntity.FirstName; persistenceUserEntity.LastName = domainUserEntity.LastName; persistenceUserEntity.Role = domainUserEntity.Role; persistenceUserEntity.Validated = domainUserEntity.Validated; // 处理子实体:更新已有,添加新的 foreach (var domainAppointment in domainUserEntity.Appointments) { if (existingAppointmentsById.TryGetValue(domainAppointment.Id, out var existingAppt)) { // 更新已有预约的属性 existingAppt.State = domainAppointment.State.Name; existingAppt.DateTime = domainAppointment.DateTime; } else { // 添加新预约到集合 persistenceUserEntity.Appointments.Add(new AppointmentEntity(domainAppointment)); } } // 移除领域对象中不再存在的预约(如果业务允许删除) foreach (var existingAppt in existingAppointmentsById.Values.ToList()) { if (!domainAppointmentsById.ContainsKey(existingAppt.Id)) { persistenceUserEntity.Appointments.Remove(existingAppt); } } // 无需调用Update:persistenceUserEntity已被上下文跟踪,变更会自动被检测 // this.context.Users.Update(persistenceUserEntity); }
额外优化建议
- 移除不必要的
Update调用:如果FindEntityById是通过EF Core的跟踪查询(比如FindAsync或带跟踪的FirstAsync)获取的实体,上下文已经在跟踪它的变更,不需要手动调用context.Users.Update,否则会强制将整个实体标记为Modified,生成不必要的UPDATE语句。 - 遵循DDD规范修改测试代码:测试中直接修改持久化类
ApplicationUserEntity不符合DDD实践,应该先操作领域对象再传给仓库更新:
// 改进后的测试Given阶段 var domainUser = await this.dbDataFactory.InsertValidatedLifeAssistant() .ToDomainEntity(new AppointmentStateFactory()); domainUser.Appointments.Add(new Appointment( Guid.NewGuid(), AppointmentState.Planned, DateTime.Today.AddDays(3)));
内容的提问来源于stack exchange,提问作者Ombrelin
相关产品推荐
相关产品推荐

