PAYATHON 2026

通过父实体导航属性添加 Element,为何保存时出现并发异常?

数据小陈

结论

可以通过父实体的 Elements 导航属性添加新元素。遇到问题时,先检查新 Element 在保存前的实体状态和主键生成配置。

改用 AddRangeAsync 后成功,一种可能的原因是:EF Core 检测导航属性变化时,将新对象判定为已有实体并准备执行 UPDATE。如果数据库中没有对应记录,就会出现预期影响 1 行、实际影响 0 行的异常。显式调用 AddRangeAsync 会将新增实体标记为 Added,安排插入。

不过,现有代码没有提供 Element 的主键定义、映射配置或实际 SQL,不能直接断定 Guid 就是主键,也不能认定异常一定由它引起。

为什么加入导航集合后不一定执行 INSERT?

questionnaire.Elements.Add(...) 首先改变的是对象之间的关系。对于普通集合,这个调用不会像 DbSet.Add(...) 一样直接声明实体需要插入。

在通常的跟踪查询和自动变更检测配置下,EF Core 会在检测变化时发现新加入的对象,再根据键值和映射规则确定其状态。

当以下条件同时出现时,需要特别留意:

  • Element 使用配置为自动生成值的主键。
  • 创建对象时,该主键已经被赋予非默认值。
  • 对象通过已跟踪实体的导航属性被发现。

在这种情况下,新发现的实体可能被标记为 Modified。即使对象由 new Element() 创建,也不能仅凭这一点判断它会被插入。

原代码中的 Guid = elementDto.Guid 值得检查,但只有确认 Guid 是主键或参与主键后,才能判断它是否与这个机制有关。如果实际主键是另一个属性,应检查该属性的值和生成配置。

这属于实体跟踪和保存行为,与 LINQ 查询语法没有直接关系。

先检查保存前的状态

添加元素之后,调用 SaveChangesAsync 之前,可以显式触发变更检测并查看状态:

context.ChangeTracker.DetectChanges();

foreach (var entry in context.ChangeTracker.Entries<Element>())
{
    Console.WriteLine($"Guid: {entry.Entity.Guid}, State: {entry.State}");
}

重点观察本次新建的对象:

  • Added:EF Core 准备插入。
  • Modified:EF Core 准备更新。
  • Unchanged:当前没有安排插入或更新。

还可以查看更完整的跟踪信息:

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

如果新元素显示为 Modified,可以解释它为什么可能触发更新行数为零的异常。再通过数据库命令日志确认实际发出的是 INSERT 还是 UPDATE。

如果新元素已经是 Added,应继续检查同一次保存中的其他实体。一次 SaveChangesAsync 可能同时包含新增、更新和软删除,报错的操作不一定来自新元素。

如何明确添加新元素?

对于已经确认需要新建的元素,可以继续通过导航属性建立关系,同时显式调用 DbSet.Add:

// 前提:questionnaire 已查询到,且 request.Elements 仅包含待新增元素。
foreach (var elementDto in request.Elements)
{
    var element = new Element
    {
        Guid = elementDto.Guid,
        Name = elementDto.Name,
    };

    questionnaire.Elements.Add(element);
    context.Elements.Add(element);
}

await context.SaveChangesAsync(cancellationToken);

也可以先收集待新增对象,再使用已经验证有效的方式:

var elementsToAdd = new List<Element>();

// 前提:request.Elements 仅包含待新增元素。
foreach (var elementDto in request.Elements)
{
    var element = new Element
    {
        Guid = elementDto.Guid,
        Name = elementDto.Name,
    };

    questionnaire.Elements.Add(element);
    elementsToAdd.Add(element);
}

await context.Elements.AddRangeAsync(elementsToAdd, cancellationToken);
await context.SaveChangesAsync(cancellationToken);

这里起作用的是显式声明新增状态。AddRangeAsync 的异步形式不是解决异常的关键,数据库写入仍发生在 SaveChangesAsync 时。

如果同一个请求同时包含新增和已有元素,需要先区分两类数据。已有元素应修改查询得到的受跟踪对象;只有确认需要插入的对象才调用 Add 或 AddRangeAsync。

主键配置和其他排查点

检查 Element 的主键究竟是哪一个属性,以及它是否配置了值生成规则。

如果主键由应用提供,映射配置应与这一约定一致;如果主键由 EF Core 或数据库生成,就应按对应配置创建实体。尚未确认映射方式时,不要直接删除 Guid 赋值或修改值生成设置。

同时确认问卷查询是否启用了跟踪。Include 负责加载关联数据,跟踪行为还会受到 AsNoTracking 和上下文默认配置的影响。对未跟踪对象修改导航集合,通常不能直接依赖当前上下文自动保存。

这条异常消息也可能对应真实的并发问题,例如已有记录被其他操作删除,或者并发令牌不再匹配。只有确认报错实体、实体状态和实际 SQL,才能区分这些情况。

备注:内容仅供参考。