Entity Framework Code First 插入数据时出现 NULL 值
结论
问题不在 SaveChanges(),而是继承模型的映射方式。
MyValue 和 MyType 在 PaymentComponent 中被声明为抽象属性,并由各派生类分别重写。Entity Framework 可能会把这些重写后的属性识别为不同派生类型各自的成员。
默认使用 TPH(Table-per-Hierarchy)继承映射时,所有类型都保存在同一张表中,但每个派生类型有各自的列。保存某一种类型时,其他派生类型对应的列自然会是 NULL。
添加下面的属性后:
public DbSet<ClubCardPayment> ClubCardPayments { get; set; }
数据库中出现更多列,是因为 ClubCardPayment 被明确加入了 EF 模型。EF 随后为它识别到的派生属性生成了对应的列。
如果 MyValue 和 MyType 是所有支付组件共有的数据,应把它们定义为基类中的普通可映射属性,不要让每个派生类重复重写。
推荐修改
把公共状态放到 PaymentComponent 中:
public abstract class PaymentComponent
{
public int PaymentComponentID { get; set; }
public int MyValue { get; set; }
public string MyType { 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;
}
}
Payment 中的导航属性应使用明确的类型:
public class Payment
{
public int PaymentID { get; set; }
public virtual ICollection<PaymentComponent> PaymentComponents { get; set; }
public DateTime PayedTime { get; set; }
public Payment()
{
PaymentComponents = new HashSet<PaymentComponent>();
}
}
两个派生类型都应明确注册到 EF 模型中:
public class NerdDinners : DbContext
{
public NerdDinners(string connString)
: base(connString)
{
}
public DbSet<Payment> Payments { get; set; }
public DbSet<PaymentComponent> PaymentComponents { get; set; }
public DbSet<GiftCouponPayment> GiftCouponPayments { get; set; }
public DbSet<ClubCardPayment> ClubCardPayments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
base.OnModelCreating(modelBuilder);
}
}
同时引入相应的命名空间:
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
插入代码可以继续采用类似的写法:
using (var db = new NerdDinners(connectionString))
{
var giftCouponPayment = new GiftCouponPayment
{
MyValue = 250,
MyType = "GiftCouponPayment"
};
var clubCardPayment = new ClubCardPayment
{
MyValue = 5000,
MyType = "ClubCardPayment"
};
var payment = new Payment
{
PayedTime = DateTime.Now,
PaymentComponents = new List<PaymentComponent>
{
giftCouponPayment,
clubCardPayment
}
};
db.Payments.Add(payment);
db.SaveChanges();
}
修改后,MyValue 和 MyType 只会映射到一组公共列,不会再为每个派生类型分别生成一组可空列。
更合适的类型标识方式
如果 MyType 只用于区分 GiftCouponPayment 和 ClubCardPayment,通常没有必要手动维护这个字符串。TPH 映射可以通过 discriminator 列区分实体类型:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<PaymentComponent>()
.Map<GiftCouponPayment>(m =>
m.Requires("PaymentComponentType")
.HasValue("GiftCoupon"))
.Map<ClubCardPayment>(m =>
m.Requires("PaymentComponentType")
.HasValue("ClubCard"));
base.OnModelCreating(modelBuilder);
}
这样就可以从领域模型中删除 MyType:
public abstract class PaymentComponent
{
public int PaymentComponentID { get; set; }
public int MyValue { get; set; }
public abstract int GetEffectiveValue();
}
数据库中的 PaymentComponentType 会由 EF 根据实体的实际类型填写,从而避免字符串值与对象类型不一致。
如果派生类确实需要不同属性
如果两个派生类中的值在业务上不是同一个概念,就不应统一命名为 MyValue,而应分别定义含义明确的属性:
public class GiftCouponPayment : PaymentComponent
{
public int CouponValue { get; set; }
public override int GetEffectiveValue()
{
return CouponValue;
}
}
public class ClubCardPayment : PaymentComponent
{
public int CardValue { get; set; }
public override int GetEffectiveValue()
{
return CardValue;
}
}
使用默认 TPH 映射时,表结构会类似下面这样:
PaymentComponentID
PaymentComponentType
CouponValue
CardValue
保存 GiftCouponPayment 时,CardValue 为 NULL;保存 ClubCardPayment 时,CouponValue 为 NULL。这是 TPH 的正常表现,并不表示数据丢失。
如果不希望表中保留这些派生类型专用的可空列,可以改用 TPT(Table-per-Type),让每个派生类型使用自己的表:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<PaymentComponent>()
.ToTable("PaymentComponent");
modelBuilder.Entity<GiftCouponPayment>()
.ToTable("GiftCouponPayment");
modelBuilder.Entity<ClubCardPayment>()
.ToTable("ClubCardPayment");
base.OnModelCreating(modelBuilder);
}
TPT 使用多张表和共享主键,查询派生类型时通常需要连接这些表。是否采用 TPT,应根据数据库结构和查询需求决定,不必只为了消除几个正常的 NULL 而更换映射方式。
注意事项
修改实体继承结构或映射配置后,现有数据库不会自动调整为新的结构。需要通过 EF Migration 更新数据库;如果是无须保留数据的开发环境,也可以重新创建数据库。
还需要注意以下几点:
- 所有需要持久化的派生实体类型都必须加入 EF 模型,可以通过
DbSet<T>或modelBuilder.Entity<T>()注册。 - 不建议使用私有字段和重写属性承载所有派生类共有的数据。公共持久化状态应直接定义在基类中。
- 如果
MyType只是实体类型名称,应优先使用 EF 的 discriminator,避免手动赋值。 - 使用 TPH 时,派生类型专用列为
NULL属于正常情况。只有公共属性对应的列为NULL,才说明映射或赋值可能存在问题。
备注:内容仅供参考。
