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

添加多关系Owned Entity时EF Core执行UPDATE而非INSERT引发并发异常

EF Core添加Owned Entity时执行UPDATE而非INSERT引发并发异常

添加Owned Entity时,EF Core执行UPDATE语句而非INSERT,导致并发异常。变更追踪器显示已跟踪新增实体,但实际执行的是UPDATE操作,无对应数据时触发异常。

简化类定义

public abstract class PaymentDemand
{
   public Guid Id { get; set; }

   public SettlementTransactions? SettlementTransactions { get; set; }
    // 更多属性
}

public class SettlementTransactions
{
   public List<SettlementAllowance> ConsumedAllowances { get; set; } = new List<SettlementAllowance>();
   public List<SettlementCharge> GeneratedCharges { get; set; } = new List<SettlementCharge>();
}

public class SettlementAllowance
{
    public Guid PaymentDemandId { get; set; }
    public Guid AccountTransactionId { get; set; } // 主键字段

    /// <summary>The type of the transaction.</summary>
    public string TransactionType { get; set; }
    // 更多属性
}

实体配置

public void Configure(EntityTypeBuilder<PaymentDemand> builder)
{
        builder.HasKey(p => p.Id);
        builder.Property(p => p.Id).ValueGeneratedNever();

        builder.OwnsOne(p => p.SettlementTransactions, transactionBuilder =>
        {
            transactionBuilder.OwnsMany(a => a.ConsumedAllowances, allowanceBuilder =>
            {
                allowanceBuilder.WithOwner().HasForeignKey(t => t.PaymentDemandId);
                allowanceBuilder.HasKey(p => p.AccountTransactionId);
                allowanceBuilder.Property(p => p.Amount).HasColumnType("Money");
                allowanceBuilder.ToTable("SettlementAllowances");
            });

            transactionBuilder.OwnsMany(c => c.GeneratedCharges, chargesBuilder =>
            {
                chargesBuilder.WithOwner().HasForeignKey(t => t.PaymentDemandId);
                chargesBuilder.HasKey(p => p.AccountTransactionId);
                chargesBuilder.Property(p => p.AccountTransactionId).ValueGeneratedNever();
                chargesBuilder.Property(p => p.Amount).HasColumnType("Money");
                chargesBuilder.ToTable("SettlementCharges");
            });
        });

        builder.Navigation(t => t.SettlementTransactions).IsRequired();
}

保存逻辑代码

public async Task DoThings(DbContext context, Guid id)
{
        var demand = await context.PaymentDemands
            .Include(st => st.SettlementTransactions)
            .FirstOrDefaultAsync(d => d.Id == id, default);

        if (demand == null)
        {
            throw new InvalidOperationException();
        }

        demand.SettleDate = DateTime.UtcNow;
        demand.SettlementTransactions ??= new SettlementTransactions();
        demand.SettlementTransactions.ConsumedAllowances.Add(new SettlementAllowance{ TransactionType = "Blah" });
        await context.SaveChangesAsync(); // 抛出异常
}

日志信息

变更追踪日志

DetectChanges starting for 'SqlBillingContext'.
      2 entities were added and 0 entities were removed from navigation 'SettlementTransactions.ConsumedAllowances' on entity with key '{PaymentDemandId: d00544fe-c6c5-4b1d-a438-81508332af2d}'.
      Context 'SqlBillingContext' started tracking 'SettlementAllowance' entity with key '{AccountTransactionId: 296596b3-d45d-4753-b2f5-0ba7566f6800}'.
      Context 'SqlBillingContext' started tracking 'SettlementAllowance' entity with key '{AccountTransactionId: 7395965f-7fda-46d9-9e3b-3b428d4a7dce}'.

执行SQL日志

Executing update commands individually as the number of batchable commands (2) is smaller than the minimum batch size (4).
      Executing DbCommand [Parameters=[@p11='296596b3-d45d-4753-b2f5-0ba7566f6800', @p0='2022-10-25T09:34:41.7143804+00:00', @p1='49.5' (Precision = 3) (Scale = 1), @p2=NULL (Size = 4000), @p3='2022-12-24T09:18:36.3176378+00:00' (Nullable = true), @p4='6ca8c244-4438-4476-b417-ed501c378c0e' (Nullable = true), @p5='12032' (Nullable = true), @p6='d00544fe-c6c5-4b1d-a438-81508332af2d', @p7='df99b62b-d1b6-4d58-8c69-4df7eef33b61' (Nullable = true), @p8='d00544fe-c6c5-4b1d-a438-81508332af2d' (Nullable = true), @p9='2022-11-24T09:18:37.3176378+00:00' (Nullable = true), @p10='1'], CommandType='Text', CommandTimeout='30']

      SET NOCOUNT ON;
      UPDATE [SettlementAllowances] SET [AccountingTime] = @p0, [Amount] = @p1, [Description] = @p2, [EndTime] = @p3, [InvoiceId] = @p4, [InvoiceNumber] = @p5, [PaymentDemandId] = @p6, [PaymentId] = @p7, [SourcePaymentDemandId] = @p8, [StartTime] = @p9, [TransactionType] = @p10
      WHERE [AccountTransactionId] = @p11;
      SELECT @@ROWCOUNT;

排查思路与解决方案

  • 确认主键赋值与生成策略:SettlementAllowance的主键AccountTransactionId配置为ValueGeneratedNever(),EF不会自动生成主键值。如果新增实例时未显式设置唯一Guid,EF可能使用默认Guid值(如全0),或该Guid已存在于数据库中,导致EF误判实体为已存在,触发UPDATE。
    • 解决:新增SettlementAllowance时显式生成唯一Guid:
      demand.SettlementTransactions.ConsumedAllowances.Add(new SettlementAllowance
      {
          AccountTransactionId = Guid.NewGuid(),
          TransactionType = "Blah",
          PaymentDemandId = demand.Id
      });
      
      或修改主键配置为自动生成(若数据库支持):
      allowanceBuilder.Property(p => p.AccountTransactionId).ValueGeneratedOnAdd();
      
  • 验证实体追踪状态:在SaveChangesAsync()前,检查新增实体的状态是否为Added:
    var newAllowance = new SettlementAllowance{ TransactionType = "Blah", AccountTransactionId = Guid.NewGuid() };
    demand.SettlementTransactions.ConsumedAllowances.Add(newAllowance);
    Console.WriteLine(context.Entry(newAllowance).State); // 应输出Added
    
    若状态为Modified,手动设置为Added:
    context.Entry(newAllowance).State = EntityState.Added;
    
  • 检查Owned Entity关系配置:OwnsMany搭配WithOwner()时,确保外键关联逻辑正确。当前配置中ConsumedAllowances的外键是PaymentDemandId,需确认新增实体时该值已正确关联到PaymentDemand的Id,避免EF无法识别归属关系而误判状态。
  • 升级EF Core版本:部分旧版本EF Core在处理Owned Entity集合时存在Bug,升级到最新稳定版可修复已知问题。
  • 避免导航属性延迟加载问题:确保查询PaymentDemand时已通过Include(st => st.SettlementTransactions)加载导航属性,防止新增时导航属性未被正确追踪。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 08:55:15