PAYATHON 2026

Entity Framework 如何避免生成 Discriminator 列?

支付阿杰

结论

Discriminator 是 Entity Framework 使用 TPH(Table Per Hierarchy,整个继承层次共用一张表)映射继承关系时自动生成的类型标识列。只要 PaymentComponent、GiftCouponPayment 和 ClubCardPayment 仍映射到同一张表,就无法直接删除这个字段。

如果不想使用 Discriminator,可以改为 TPT(Table Per Type)映射,让基类和各个派生类分别对应一张表。继承映射更适合通过 Fluent API 配置。旧版 Entity Framework 对相关 attributes 的支持有限,而且 EntityFramework.dll 中显示的 Runtime version 也不是 Entity Framework 的产品版本。

如果只是想指定外键列名,可以显式声明 PaymentID 属性,再使用 [ForeignKey] 或 Fluent API 将它与 Payment 导航属性关联。

为什么会生成 Discriminator

下面两个类都继承了 PaymentComponent:

public class GiftCouponPayment : PaymentComponent
{
}

public class ClubCardPayment : PaymentComponent
{
}

没有指定其他继承映射方式时,Code First 默认使用 TPH。数据库只创建一张表来保存整个继承层次中的数据,因此需要一个类型标识列来区分每条记录实际对应哪个派生类。这个列默认叫作 Discriminator。

删除 MyType 属性并不会让 Discriminator 消失。MyType 只是一个普通的实体属性,Entity Framework 不会自动将它用作继承映射的类型标识。

使用 TPT 避免 Discriminator

可以在 OnModelCreating 中明确指定每种类型对应的表:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<PaymentComponent>()
        .ToTable("PaymentComponents");

    modelBuilder.Entity<GiftCouponPayment>()
        .ToTable("GiftCouponPayments");

    modelBuilder.Entity<ClubCardPayment>()
        .ToTable("ClubCardPayments");

    base.OnModelCreating(modelBuilder);
}

配置后会得到类似这样的表结构:

  • PaymentComponents 保存 PaymentComponentID、MyValue、MyType、PaymentID 等基类字段。
  • GiftCouponPayments 保存礼券支付类型特有的字段,并通过 PaymentComponentID 关联基表。
  • ClubCardPayments 保存会员卡支付类型特有的字段,并通过 PaymentComponentID 关联基表。

派生类型可以通过记录所在的派生表来判断,因此不再需要 Discriminator。

即使派生类没有自己的持久化属性,TPT 仍会为每个派生类创建一张只包含主键的表。这需要做些取舍:为了删除一个 Discriminator 列而增加多张表和额外的查询连接,通常不一定值得。如果几种支付类型的区别只在于计算逻辑,继续使用默认的 TPH 会更简单。

将外键命名为 PaymentID

可以在依赖实体 PaymentComponent 中显式声明外键和导航属性:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public abstract class PaymentComponent
{
    public int PaymentComponentID { get; set; }

    public int MyValue { get; set; }

    public string MyType { get; set; }

    public int PaymentID { get; set; }

    [ForeignKey("PaymentID")]
    public virtual Payment Payment { get; set; }

    public abstract int GetEffectiveValue();
}

Payment 类中的集合导航属性可以这样定义:

public class Payment
{
    public Payment()
    {
        PaymentComponents = new HashSet<PaymentComponent>();
    }

    public int PaymentID { get; set; }

    public DateTime PayedTime { get; set; }

    public virtual ICollection<PaymentComponent> PaymentComponents { get; set; }
}

属性名 PaymentID 已符合 Code First 的外键命名约定,多数版本即使省略 [ForeignKey] 也能正确识别。保留这个 attribute,则可以让属性与导航关系更加明确。

也可以全部改用 Fluent API:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<PaymentComponent>()
        .ToTable("PaymentComponents");

    modelBuilder.Entity<GiftCouponPayment>()
        .ToTable("GiftCouponPayments");

    modelBuilder.Entity<ClubCardPayment>()
        .ToTable("ClubCardPayments");

    modelBuilder.Entity<PaymentComponent>()
        .HasRequired(component => component.Payment)
        .WithMany(payment => payment.PaymentComponents)
        .HasForeignKey(component => component.PaymentID)
        .WillCascadeOnDelete(false);

    base.OnModelCreating(modelBuilder);
}

如果 PaymentComponent 可以暂时不属于任何 Payment,应将外键改成可空类型,并把关系配置为可选:

public int? PaymentID { get; set; }

public virtual Payment Payment { get; set; }
modelBuilder.Entity<PaymentComponent>()
    .HasOptional(component => component.Payment)
    .WithMany(payment => payment.PaymentComponents)
    .HasForeignKey(component => component.PaymentID);

完整示例

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;

public abstract class PaymentComponent
{
    public int PaymentComponentID { get; set; }

    public int MyValue { get; set; }

    public string MyType { get; set; }

    public int PaymentID { get; set; }

    [ForeignKey("PaymentID")]
    public virtual Payment Payment { get; set; }

    public abstract int GetEffectiveValue();
}

public class GiftCouponPayment : PaymentComponent
{
    public override int GetEffectiveValue()
    {
        return MyValue;
    }
}

public class ClubCardPayment : PaymentComponent
{
    public override int GetEffectiveValue()
    {
        return MyValue;
    }
}

public class Payment
{
    public Payment()
    {
        PaymentComponents = new HashSet<PaymentComponent>();
    }

    public int PaymentID { get; set; }

    public DateTime PayedTime { get; set; }

    public virtual ICollection<PaymentComponent> PaymentComponents { get; set; }
}

public class NerdDinners : DbContext
{
    public NerdDinners(string connectionString)
        : base(connectionString)
    {
    }

    public DbSet<PaymentComponent> PaymentComponents { get; set; }

    public DbSet<GiftCouponPayment> GiftCouponPayments { get; set; }

    public DbSet<ClubCardPayment> ClubCardPayments { get; set; }

    public DbSet<Payment> Payments { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PaymentComponent>()
            .ToTable("PaymentComponents");

        modelBuilder.Entity<GiftCouponPayment>()
            .ToTable("GiftCouponPayments");

        modelBuilder.Entity<ClubCardPayment>()
            .ToTable("ClubCardPayments");

        modelBuilder.Entity<PaymentComponent>()
            .HasRequired(component => component.Payment)
            .WithMany(payment => payment.PaymentComponents)
            .HasForeignKey(component => component.PaymentID)
            .WillCascadeOnDelete(false);

        base.OnModelCreating(modelBuilder);
    }
}

关于 attributes 的限制

外键名称可以通过 [ForeignKey] 明确配置。部分 Entity Framework 版本也能结合 [Table] 完成继承映射,但不同旧版本的支持情况并不一致,因此使用 Fluent API 通常更清楚、更可靠。

另外,EntityFramework.dll 显示的 v4.0.30XXX 通常指 CLR Runtime version,不能用来准确判断当前使用的是 EF 4.1、4.2、4.3 还是其他版本。应检查程序集的实际版本或 NuGet 包版本,再确认 [Table]、[ForeignKey] 和迁移功能是否可用。

修改映射后,已有数据库不会自动删除原来的 Discriminator,也不会自动重命名外键列。需要重新创建数据库,或者生成并执行对应的数据库迁移。

备注:内容仅供参考。