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

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

疑问

  1. 为何会出现该问题?
  2. 能否保留时态表中的原始Id进行插入?

问题解答

一、错误原因分析

  • 实体状态与追踪问题:从时态表查询的historicalUser是通过AsNoTracking()获取的,所以关联的UserImage和UserImageStat都处于Detached状态。初始映射器直接将历史集合赋值给当前用户的Images属性时,EF Core无法正确识别这些实体的状态,只能将其视为待插入的新实体,但它们的Id和UserImageId还是时态表中的旧值。
  • 外键引用无效:当前数据库中不存在旧Id对应的UserImage记录,EF Core尝试插入UserImageStat时,引用的UserImageId是一个不存在的值,直接触发外键约束冲突。即使EF Core通常会处理插入顺序,这里的外键值本身就无效,顺序再对也解决不了问题。
  • 集合赋值的关联丢失:直接替换集合的操作,会让EF Core无法追踪UserImage和UserImageStat之间的关联关系,进一步导致插入顺序判断错误。

二、能否保留时态表中的原始Id插入?

可以,但需要满足两个前提并做相应处理:

  1. 前提:原始Id未被占用:当前数据库中不存在相同Id的UserImage记录,否则会触发主键冲突。
  2. 手动修正实体状态与关联:
    • 不要直接替换集合,改用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;
      }
      
    这样EF Core能正确识别两者的关联,先插入UserImage,再插入UserImageStat,此时外键引用的是刚插入的有效Id,不会触发冲突。

需要注意:如果回滚的是已删除的记录,且原始Id已被其他新记录占用,就必须生成新Id,否则会出现主键冲突。

内容的提问来源于stack exchange,提问作者sion_corn

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 08:07:55