EF Core脚手架:带有WHERE条件的唯一索引会产生1:1的映射,而不是1:n

后端开发 2026-07-12

我们正在使用.NET 10以及以下的NuGet包:

Microsoft.EntityFrameworkCore 10.0.3
Microsoft.EntityFrameworkCore.Design 10.0.3
Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0

我们有一个简单的PostgreSQL数据库,其中包含2 张表:trgt.targettrgt.target_request。一个目标可以有多个目标请求,但同时只有一个处于活动状态(唯一索引 ux_target_request_active_per_target)。

-- 1. create schema
create schema if not exists trgt;

-- 2. create table trgt.target
create table trgt.target 
(
    id serial primary key,
    name varchar(255) not null
);

-- 3. create table trgt.target_request
create table trgt.target_request 
(
    id serial primary key,
    target_id integer not null,
    is_active boolean not null,
    constraint fk_target_request_target
        foreign key (target_id)
        references trgt.target(id)
        on delete cascade
);

-- 4. ensure only one active request per target
create unique index ux_target_request_active_per_target
on trgt.target_request (target_id)
where is_active = true;

我们采取数据库优先的方法——将数据库表生成为C#模型。我们使用以下命令来脚手架 trgt 架构:

dotnet ef dbcontext scaffold "Host=localhost;Database=UniqueIndexScaffold;Username=postgres;Password=postgres" Npgsql.EntityFrameworkCore.PostgreSQL -c TrgtDbContext -o Models --schema trgt --force --verbose --data-annotations --no-onconfiguring

生成的 TrgtDbContext.cs 的内容如下:

public partial class TrgtDbContext : DbContext
{
    public TrgtDbContext(DbContextOptions<TrgtDbContext> options)
        : base(options)
    {
    }

    public virtual DbSet<Target> Targets { get; set; }
    public virtual DbSet<TargetRequest> TargetRequests { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Target>(entity =>
        {
            entity.HasKey(e => e.Id).HasName("target_pkey");
        });

        modelBuilder.Entity<TargetRequest>(entity =>
        {
            entity.HasKey(e => e.Id).HasName("target_request_pkey");

            entity.HasIndex(e => e.TargetId, "ux_target_request_active_per_target")
                .IsUnique()
                .HasFilter("(is_active = true)");

            entity.HasOne(d => d.Target).WithOne(p => p.TargetRequest).HasConstraintName("fk_target_request_target");
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

生成的 Target.cs 文件的内容如下:

[Table("target", Schema = "trgt")]
public partial class Target
{
    [Key]
    [Column("id")]
    public int Id { get; set; }

    [Column("name")]
    [StringLength(255)]
    public string Name { get; set; } = null!;

    [InverseProperty("Target")]
    public virtual TargetRequest? TargetRequest { get; set; }
}

我们觉得它包含属性 public virtual TargetRequest? TargetRequest 十分奇怪。尽管我们有一个唯一索引 ux_target_request_active_per_target,它允许每个目标只有一个 活跃的 目标请求,但该目标仍然可能拥有多个 非活跃的 目标请求。

如果我们移除 ux_target_request_active_per_target 的唯一索引并再次对数据库进行脚手架,Target.cs 将包含一个属性 public virtual ICollection<TargetRequest> TargetRequests,这是预期之中的。我们也希望在存在索引 ux_target_request_active_per_target 时,恰好也会出现同样的属性。

这是脚手架中的一个缺陷吗?有没有解决方法?

解决方案

首先,这是一个已知缺陷。EF Core Power Tools的 Reverse Engineer工具可以通过使用 excludedIndexes 选项来忽略该索引从而处理这个问题。这样会生成一个集合,但不会验证有多少项符合筛选条件。

其次,在EF Core中并不存在所谓的“数据库优先”(DB First)模式。脚手架器仅仅做它的名字所述的事——从现有数据库生成(脚手架)类,作为一种方便的方式。文档故意使用 Reverse Engineerscaffolding 这两个术语。你将不得不以某种方式对模型进行修改。对象模型和关系模型差异极大,无法用神奇的方式将它们映射到对方。也并非可取——每个模型都有其自身希望具备的特性。你可以使用自定义代码生成模板来改变代码的创建方式,但在某些阶段,你最终会修改生成的实体和DbContext。

也就是说,将此模式映射到另一种模式没有简单的方法。

  • 拥有一个 List<TargetRequest> 属性并不能保证只有其中一个会将 TargetRequest.IsActive 设为真。调用 SaveChanges 时,你如何确保实体的有效性?你可能需要实现 IValidateObject,但EF Core在保存时并不执行验证。应用程序需要对实体进行验证。
  • 何时对其进行验证?是在修改 List<TargetRequest> 时,还是只有在保存时?对每次修改进行验证可能会让编辑变得非常困难——你必须始终在激活另一个请求之前先停用当前请求。想象在UI中绑定到一组单选按钮。根据UI事件的顺序,应用可能会抛出异常。
  • 如何访问活动请求?也许一个 ActiveRequest=>TargetRequests.Single(t->IsActive); 属性就能解决问题,在状态错误时给出提示。

如果你决定生成什么,可以使用 自定义脚手架模板 或EF Core Power Tools来定制生成的代码。

或者你也可以手动修改实体,例如使用 将默认接口方法作为混入 来定义一个可以应用到你只想要的实体上的 IUniqueActive<T> 接口?也许像下面这样:

public interface IHasIsActive
{
    bool IsActive {get;set;}
}

public interface IUniqueActive<TItem> where TItem:IHasActive
{
    public List<TItem> Request {get;}

    public TItem ActiveRequest => Request.Single(it=>it.IsActive);
}
...

public partial class TargetRequest:IHasIsActive
{
    public bool IsActive {get;set;}
}
public partial class Target:IUniqueActive<TargetRequest>
{

    public List<TargetRequest> Requests {get;set;}
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章