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

批量添加Vehicle时Color.Red静态属性为null触发PostgreSQL非空约束异常

批量添加Vehicle时Color静态属性被设为null触发数据库非空约束异常

问题现象

使用Color.Red静态属性创建Vehicle实体,批量添加到PostgreSQL数据库时,color字段被设为null,触发非空约束异常。单个添加Vehicle并保存时一切正常。

异常信息

Npgsql.PostgresException
23502: null value in column "color" of relation "vehicle" violates not-null constraint

DETAIL: Failing row contains (25134bbd-2420-4b61-8ec8-e484cddce50b, 6778fe55-7596-4c22-aa29-43247d6d59a9, 1, К333ОТ77, null, SALYA2BN2KA791786, 0, 10, 10).

相关代码

创建Vehicle的代码

var model = Domain.ModelAggregate.Model.Create(Brand.Create("Kia"), CarModel.Create("Rio"),
            Category.Create('B'), Tariff.Create(1, 2, 3));
        
await Context.Models.AddAsync(model);
await Context.SaveChangesAsync();
        
var vehicles = Enumerable.Range(0, vehiclesCount)
    .Select(_ => Domain.VehicleAggregate.Vehicle.Create(model.Id, PlateNumber.Create("К333ОТ77"), Color.Red,
        Vin.Create("SALYA2BN2KA791786"), Location.Create(10.0, 10.0), FuelLevel.Create()))
    .ToList();
        
Context.AttachRange(vehicles.Select(x => x.Status).ToList());
await Context.Vehicles.AddRangeAsync(vehicles);
await Context.SaveChangesAsync();

Color类定义

public sealed class Color : ValueObject
{
    public static readonly Color White = new(nameof(White).ToLowerInvariant());
    public static readonly Color Red = new(nameof(Red).ToLowerInvariant());
...
}

DbContext及配置

public sealed class DataContext(DbContextOptions<DataContext> options) : DbContext(options)
{
    public DbSet<Vehicle> Vehicles { get; set; }
    public DbSet<Model> Models { get; set; }
    public DbSet<OutboxEvent> Outbox { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new ModelEntityTypeConfiguration());
        modelBuilder.ApplyConfiguration(new VehicleEntityTypeConfiguration());
        modelBuilder.ApplyConfiguration(new StatusEntityTypeConfiguration());
        modelBuilder.ApplyConfiguration(new OutboxEventTypeConfiguration());
    }
}

internal sealed class VehicleEntityTypeConfiguration : IEntityTypeConfiguration<Vehicle>
{
    public void Configure(EntityTypeBuilder<Vehicle> builder)
    {
        builder.ToTable("vehicle");
        
        builder.HasKey(x => x.Id);
        
        builder.Property(x => x.Id).ValueGeneratedNever().HasColumnName("id").IsRequired();
        builder.Property(x => x.ModelId).HasColumnName("model_id").IsRequired();

        builder.HasOne<Model>().WithMany().HasForeignKey(x => x.ModelId).HasConstraintName("FK_model_id").IsRequired();
        
        builder.HasOne(x => x.Status)
            .WithMany()
            .HasForeignKey("status_id")
            .HasConstraintName("FK_status_id")
            .IsRequired();

        builder.OwnsOne(x => x.PlateNumber, cfg =>
        {
            cfg.Property(x => x.Value).HasColumnName("plate_number").IsRequired();
        });

        builder.OwnsOne(x => x.Color, cfg =>
        {
            cfg.Property(x => x.Name).HasColumnName("color").IsRequired();
        });

        builder.OwnsOne(x => x.Vin, cfg =>
        {
            cfg.Property(x => x.Number).HasColumnName("vin").IsRequired();
        });

        builder.OwnsOne(x => x.FuelLevel, cfg =>
        {
            cfg.Property(x => x.Percents).HasColumnName("fuel_level_percents").IsRequired();
        });

        builder.OwnsOne(x => x.Location, cfg =>
        {
            cfg.Property(x => x.Latitude).HasColumnName("location_latitude").IsRequired();
            cfg.Property(x => x.Longitude).HasColumnName("location_longitude").IsRequired();
        });
        
        builder.Ignore(x => x.DomainEvents);
    }
}

internal sealed class StatusEntityTypeConfiguration : IEntityTypeConfiguration<Status>
{
    public void Configure(EntityTypeBuilder<Status> builder)
    {
        builder.ToTable("vehicle_status");
        
        builder.HasKey(x => x.Id);
        
        builder.Property(x => x.Id).ValueGeneratedNever().HasColumnName("id").IsRequired();
        builder.Property(x => x.Name).HasColumnName("name").IsRequired();

        builder.HasData(Status.All());
    }
}

解决方案

原因分析

EF Core批量添加实体时,对于复用的同一个值对象静态实例(如Color.Red),可能因追踪上下文的引用识别逻辑,未正确读取其属性值,导致生成SQL时传入null。单个添加时EF能正确处理,但批量场景下引用复用引发追踪异常。

修复方案

方案1:每次创建Vehicle时生成新的Color实例

避免复用静态实例,每次创建Vehicle时生成新的Color对象:

var vehicles = Enumerable.Range(0, vehiclesCount)
    .Select(_ => Domain.VehicleAggregate.Vehicle.Create(model.Id, PlateNumber.Create("К333ОТ77"), 
        new Color("red"), // 或通过工厂方法创建新实例
        Vin.Create("SALYA2BN2KA791786"), Location.Create(10.0, 10.0), FuelLevel.Create()))
    .ToList();

可给Color添加工厂方法简化创建:

public sealed class Color : ValueObject
{
    public static readonly Color White = new(nameof(White).ToLowerInvariant());
    public static readonly Color Red = new(nameof(Red).ToLowerInvariant());

    // 添加工厂方法
    public static Color Create(string name) => new(name.ToLowerInvariant());

    // 确保重写值对象的相等性判断
    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Name;
    }
}

方案2:为Color值对象配置自定义ValueComparer

在EF Core的实体配置中,为Color值对象指定ValueComparer,让EF正确识别值对象的属性:

builder.OwnsOne(x => x.Color, cfg =>
{
    cfg.Property(x => x.Name).HasColumnName("color").IsRequired();
    // 添加自定义ValueComparer
    cfg.Comparer(new ValueComparer<Color>(
        (c1, c2) => c1?.Name == c2?.Name,
        c => c?.Name.GetHashCode() ?? 0,
        c => new Color(c.Name)));
});

这个配置告诉EF如何比较Color实例的相等性,以及如何复制实例,避免批量添加时的追踪问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 04:13:10