EF Code First 无法向 GiftCouponPayment 表插入数据
结论
GiftCouponPayment 没有被插入,是因为当前代码只将 Payment 加入了 EF 上下文:
db.Payments.Add(payment);
PaymentComponents 很可能是一个以 IPaymentComponent 接口为元素类型的集合。Entity Framework Code First 无法将接口识别为可映射的实体类型,因此也无法把这个集合当作导航属性处理。即使其中包含 GiftCouponPayment,EF 也不会自动跟踪并插入该对象。
最直接的修复方法是显式添加两个实体:
db.GiftCouponPayments.Add(giftCouponPayment);
db.Payments.Add(payment);
db.SaveChanges();
如果 GiftCouponPayment 和 Payment 在数据库中确实存在关联,还应使用具体实体类型定义导航属性,并配置外键关系。
直接修复
调用 SaveChanges() 前,将 giftCouponPayment 添加到对应的 DbSet:
static void Main(string[] args)
{
string connectionString =
"Data Source=.;Initial Catalog=NerdDinners;" +
"Integrated Security=True;Connect Timeout=30";
using (var db = new NerdDinners(connectionString))
{
var giftCouponPayment = new GiftCouponPayment
{
MyID = 1,
MyValue = 250
};
var comps = new List<IPaymentComponent>
{
giftCouponPayment
};
var payment = new Payment
{
PaymentID = 1,
PaymentComponents = comps,
PayedTime = DateTime.Now
};
db.GiftCouponPayments.Add(giftCouponPayment);
db.Payments.Add(payment);
int recordsAffected = db.SaveChanges();
}
}
这样,两个对象都会进入 Added 状态,SaveChanges() 会分别生成相应的 INSERT。
需要注意的是,这段代码只是插入了两条记录,并不会自动在数据库中建立它们之间的关联。
为什么不会级联插入
EF 可以沿导航属性发现并插入整个实体图,但导航属性中的元素必须是 EF 能够映射的具体实体类型,例如:
public virtual ICollection<GiftCouponPayment> PaymentComponents { get; set; }
接口集合不能作为常规的 EF 导航属性:
public ICollection<IPaymentComponent> PaymentComponents { get; set; }
IPaymentComponent 是 CLR 接口,并不是 EF 模型中的实体类型。仅凭这个接口,EF 无法确定:
- 应该操作哪张表;
- 实际实体使用什么主键;
- 两个实体之间有什么关系;
- 应该使用哪个外键;
- 是否需要级联插入。
所以,将 GiftCouponPayment 放入接口集合,并不等于将它添加到 EF 上下文。
推荐的实体关系设计
如果一个 Payment 可以包含多个 GiftCouponPayment,可以使用具体类型的导航属性,并为它配置外键。
public class Payment
{
public Payment()
{
GiftCouponPayments = new HashSet<GiftCouponPayment>();
}
public int PaymentID { get; set; }
public DateTime PayedTime { get; set; }
public virtual ICollection<GiftCouponPayment> GiftCouponPayments
{
get;
set;
}
}
public class GiftCouponPayment : IPaymentComponent
{
public int GiftCouponPaymentID { get; set; }
public int CouponValue { get; set; }
public int PaymentID { get; set; }
public virtual Payment Payment { get; set; }
public int MyID
{
get { return GiftCouponPaymentID; }
set { GiftCouponPaymentID = value; }
}
public int MyValue
{
get { return CouponValue; }
set { CouponValue = value; }
}
public int GetEffectiveValue()
{
return CouponValue;
}
}
在 OnModelCreating 中显式配置关系:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Payment>()
.HasMany(p => p.GiftCouponPayments)
.WithRequired(g => g.Payment)
.HasForeignKey(g => g.PaymentID);
}
插入代码可以简化为:
using (var db = new NerdDinners(connectionString))
{
var payment = new Payment
{
PaymentID = 1,
PayedTime = DateTime.Now
};
payment.GiftCouponPayments.Add(new GiftCouponPayment
{
GiftCouponPaymentID = 1,
CouponValue = 250
});
db.Payments.Add(payment);
db.SaveChanges();
}
此时,EF 可以识别完整的实体关系。将 Payment 添加到上下文后,关联的 GiftCouponPayment 也会被标记为 Added,并随之插入数据库。
接口属性的处理
IPaymentComponent 仍然可以用于业务逻辑,例如统一计算不同支付组件的有效金额,但不适合直接作为 EF 导航集合的元素类型。
如果业务层必须公开接口集合,可以额外提供一个不参与映射的属性:
[NotMapped]
public IEnumerable<IPaymentComponent> PaymentComponents
{
get { return GiftCouponPayments.Cast<IPaymentComponent>(); }
}
数据库映射则继续使用具体类型:
public virtual ICollection<GiftCouponPayment> GiftCouponPayments
{
get;
set;
}
这样可以保留接口抽象,同时不影响 EF 建模。
注意事项
确认属性会被映射
如果 CouponValue 只是私有字段:
private int CouponValue;
旧版 EF Code First 通常不会将其映射为普通列。建议改为公开属性:
public int CouponValue { get; set; }
MyValue 可以只作为业务层别名,必要时添加 [NotMapped]:
[NotMapped]
public int MyValue
{
get { return CouponValue; }
set { CouponValue = value; }
}
否则可能出现记录已经插入,但金额字段没有按预期持久化的情况。
避免两个属性映射同一个含义
如果 MyID 只是 GiftCouponPaymentID 的包装属性,建议只将真正的主键映射到数据库:
[NotMapped]
public int MyID
{
get { return GiftCouponPaymentID; }
set { GiftCouponPaymentID = value; }
}
主键应明确声明:
[Key]
public int GiftCouponPaymentID { get; set; }
检查实体状态
排查类似问题时,可以在保存前查看实体状态:
var state = db.Entry(giftCouponPayment).State;
等待插入的实体应处于以下状态:
EntityState.Added
如果状态是 Detached,说明 EF 还没有跟踪该对象,需要显式添加:
db.GiftCouponPayments.Add(giftCouponPayment);
最小改动是调用 db.GiftCouponPayments.Add(...)。更完整的处理方式是将接口集合改为具体实体类型的导航集合,并为 Payment 和 GiftCouponPayment 配置明确的外键关系。
备注:内容仅供参考。
