EF Core时态表回滚时INSERT语句违反外键约束
问题背景与疑问
我有一个基于ASP.NET Core 7和EF Core 7的应用程序,实体类定义如下:
public class ApiUser : IdentityUser { ... public virtual List<UserImage> Images { get; set; } = new(); } public class UserImage : UdbObject { public Guid Id { get; set; } [ForeignKey(nameof(User))] public string UserId { get; set; } public virtual ApiUser User { get; set; } ... public virtual UserImageStat Stat { get; set; } } [PrimaryKey(nameof(UserImageId))] public class UserImageStat { [ForeignKey(nameof(Image))] public Guid UserImageId { get; set; } public virtual UserImage Image { get; set; } ... }
回滚接口实现逻辑:
[HttpPut] [Route("rollback")] public async Task<IActionResult> Rollback( [FromQuery] string userId, [FromQuery] DateTime timeOfEdit) { //Get historical user var historicalUser = await _context.Users .TemporalAsOf(timeOfEdit) .Where(x => x.Id == userId) .Include(e => e.Images).ThenInclude(img => img.Stat) .AsNoTracking() .FirstOrDefaultAsync(); // Get current user var currentStateUser = await _context.Users .Where(x => x.Id == userId) .Include(e => e.Images).ThenInclude(img => img.Stat) .Include(e => e.Stat) .FirstOrDefaultAsync(); if (historicalUser is null || currentStateUser is null) return BadRequest(); // map historical to current _userMapper.Map_HistoricalUser_To_CurrentUserAsync<PropertyInfo>(historicalUser, currentStateUser); await _context.SaveChangesAsync(); return NoContent(); }
映射器初始实现:
public void Map_HistoricalUser_To_CurrentUserAsync<T>( ApiUser historicalUser, ApiUser currentUser) where T : PropertyInfo { PropertyInfo[] properties = typeof(ApiUser).GetProperties(); foreach (T property in properties) { if (property.Name != "Id") { object value = property.GetValue(historicalUser); property.SetValue(currentUser, value); } } }
问题场景
执行Rollback时,historicalUser.Images有1条数据,currentStateUser.Images为空。映射后currentStateUser.Images包含该数据(含UserImageStat),调用SaveChangesAsync时出现错误:
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_UserImageStats_UserImages_UserImageId". The conflict occurred in database "myDb", table "dbo.UserImages", column 'Id'.
原本以为SaveChangesAsync会自动处理实体插入顺序(如依赖B的A会在B之后插入)。修改映射器,手动添加Images并重置Id为空后问题解决:
public void Map_HistoricalUser_To_CurrentUserAsync<T>( ApiUser historicalUser, ApiUser currentUser) where T : PropertyInfo { PropertyInfo[] properties = typeof(ApiUser).GetProperties(); foreach (T property in properties) { if (property.Name != "Id" && property.Name != "Images") { object value = property.GetValue(historicalUser); property.SetValue(currentUser, value); } } currentUser.Images.AddRange(historicalUser.Images); currentUser.Images.First().Id = Guid.Empty; currentUser.Images.First().Stat.UserImageId = Guid.Empty; }
疑问
- 为何会出现该问题?
- 能否保留时态表中的原始Id进行插入?
问题解答
一、错误原因分析
- 实体状态与追踪问题:从时态表查询的
historicalUser是通过AsNoTracking()获取的,所以关联的UserImage和UserImageStat都处于Detached状态。初始映射器直接将历史集合赋值给当前用户的Images属性时,EF Core无法正确识别这些实体的状态,只能将其视为待插入的新实体,但它们的Id和UserImageId还是时态表中的旧值。 - 外键引用无效:当前数据库中不存在旧Id对应的
UserImage记录,EF Core尝试插入UserImageStat时,引用的UserImageId是一个不存在的值,直接触发外键约束冲突。即使EF Core通常会处理插入顺序,这里的外键值本身就无效,顺序再对也解决不了问题。 - 集合赋值的关联丢失:直接替换集合的操作,会让EF Core无法追踪
UserImage和UserImageStat之间的关联关系,进一步导致插入顺序判断错误。
二、能否保留时态表中的原始Id插入?
可以,但需要满足两个前提并做相应处理:
- 前提:原始Id未被占用:当前数据库中不存在相同
Id的UserImage记录,否则会触发主键冲突。 - 手动修正实体状态与关联:
- 不要直接替换集合,改用
AddRange添加历史实体。 - 无需重置
UserImage.Id,但要确保UserImageStat.UserImageId与UserImage.Id一致,并手动标记实体状态:currentUser.Images.AddRange(historicalUser.Images); foreach(var image in currentUser.Images) { // 标记UserImage为新增状态 _context.Entry(image).State = EntityState.Added; // 确保Stat的外键指向当前UserImage的Id image.Stat.UserImageId = image.Id; // 标记Stat为新增状态 _context.Entry(image.Stat).State = EntityState.Added; }
UserImage,再插入UserImageStat,此时外键引用的是刚插入的有效Id,不会触发冲突。 - 不要直接替换集合,改用
需要注意:如果回滚的是已删除的记录,且原始Id已被其他新记录占用,就必须生成新Id,否则会出现主键冲突。
内容的提问来源于stack exchange,提问作者sion_corn
相关产品推荐
相关产品推荐

