EF 代码优先:一对多两次到同一集合类型
本文关键字:两次 集合 类型 集合类 代码 一对多 EF | 更新日期: 2023-09-27 18:35:20
简化:在我的数据库中,我有一个产品在不同的日期以不同的价格出售。换句话说,它有一个价格历史。我有两个类:产品和价格,具有一对多关系:
public class Product
{
public int ProductId {get; set;}
public string Name {get; set;}
public ICollection<Price> Prices {get; set;}
}
public class Price
{
public int PriceId {get; set;}
// foreign key to Product:
public int ProductId {get; set;}
public Product Product {get; set;}
public DateTime ActivationDate {get; set;}
public decimal value {get; set;}
}
public class MyDbContext : DbContext
{
public DbSet<Price> Prices { get; set; }
public DbSet<Product> Products { get; set; }
}
到目前为止一切顺利,实体框架知道如何处理这个问题。通过使用这两个类,我能够获得某个特定日期的某个产品的价格。
但是,如果我的产品有两个价格历史记录:购买价格历史记录和零售价格历史记录,该怎么办?
public class Product
{
public int ProductId {get; set;}
public string Name {get; set;}
public ICollection<Price> PurchasePrices {get; set;}
public ICollection<Price> RetailPrices {get; set;}
}
因为这两个集合是同一类型的,所以我不希望单独的表填充相同类型的对象(真正的原因是:我有很多带有价格集合的类)。
所以我必须使用Fluent API进行一些编码。我的直觉告诉我,我需要连接表,就像在多对多关系中一样,可以使用ManyToManyNavigationPropertyConfiguration.Map。
怎么做?
在阅读了有关一对一外键关联的故事并将其用于一对多关联后,我能够按照以下要求实现它:
- 我可以有许多不同的类,具有相同类型 T 的属性
- 所有实例类型 T 都可以放在一个表中,即使此类型的"所有者"位于不同的表中也是如此。
- 一个类甚至可以有两个 T 类型的属性。
例如:客户可能有一个账单地址和一个送货地址,两者都是地址类型。两个地址可以放在一个表中:地址。
public class Address
{
public int Id { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public int BillingAddressId { get; set; }
public Address BillingAddress { get; set; }
public int DeliveryAddressId { get; set; }
public Address DeliveryAddress { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Address> Addresses { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasRequired(p => p.DeliveryAddress)
.WithMany()
.HasForeignKey(p => p.DeliveryAddressId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<User>()
.HasRequired(p => p.BillingAddress)
.WithMany()
.HasForeignKey(p => p.BillingAddressId)
.WillCascadeOnDelete(false);
}
}
此解决方案中的聪明之处在于地址中没有"拥有"用户。因此,如果我使用地址定义一个新类,则可以将此地址添加到同一地址表中。因此,如果我有十个不同的类,它们都有一个地址,我就不需要十个地址表。
如果您有地址集合该怎么办?
通常,在一对多关系中,多方需要对一侧的外键以及对"所有者"的引用:
一个经常看到的例子:博客和帖子:一个博客有很多帖子。一篇文章只属于一个博客:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
virtual public ICollection<Post> Posts {get; set;}
}
public class Post
{
public int Id { get; set; }
public string Text { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
此命名将自动导致正确的一对多关系,但如果要在 DbContext 中指定:
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
在 OnModelCreation 中:
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithRequired(post => post.Blog)
.HasForeignKey(post => post.BlogId);
即使不需要 Post.Blog,也无法删除此属性,因为模型创建。如果你删除它,你最终会得到魔术字符串来定义外键。
为了能够同时拥有地址集合(或者在我最初的问题中:很多价格历史,其中每个价格历史都是价格的集合),我结合了这两种方法。
public class Price
{
public int Id { get; set; }
public int PriceHistoryId { get; set; }
public virtual PriceHistory PriceHistory { get; set; }
public DateTime ActivationDate { get; set; }
public decimal Value { get; set; }
}
public class PriceHistory
{
public int Id { get; set; }
virtual public ICollection<Price> Prices { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// Purchase Prices
public virtual PriceHistory PurchasePriceHistory { get; set; }
public int PurchasePriceHistoryId { get; set; }
// Retail prices
public virtual PriceHistory RetailPriceHistory { get; set; }
public int RetailPriceHistoryId { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<PriceHistory> PriceHistories { get; set; }
public DbSet<Price> Prices { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// one price history has many prices: one to many:
modelBuilder.Entity<PriceHistory>()
.HasMany(p => p.Prices)
.WithRequired(price => price.PriceHistory)
.HasForeignKey(price => price.PriceHistoryId);
// one product has 2 price histories, the used method is comparable
// with the method user with two addresses
modelBuilder.Entity<Product>()
.HasRequired(p => p.PurchasePriceHistory)
.WithMany()
.HasForeignKey(p => p.PurchasePriceHistoryId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Product>()
.HasRequired(p => p.RetailPriceHistory)
.WithMany()
.HasForeignKey(p => p.RetailPriceHistoryId)
.WillCascadeOnDelete(false);
}
}
我已经用其他具有多个价格历史的类对其进行了测试: - 所有价格将在一个表格中 - 所有价格历史将集中在一个表格中 - 每个对价格历史的引用都需要一个价格历史 ID。
如果您仔细观察结果,它实际上是多对多关系的实现,其中价格历史是耦合表。
我试图删除价格历史类,并让产品在 OnModelCreation 中具有多对多的价格集合,但这会导致带有魔术字符串的"Map"语句,以及每个价格历史的单独表。
有关如何实现多对多的说明的链接
由于 EF 命名约定,您的代码目前仍在工作:
如果类上的属性命名为"ID"(不区分大小写),或者类名后跟"ID",则 Code First 推断属性是主键。如果主键属性的类型是数字或 GUID,则会将其配置为标识列。
EF 看到你有一对多,因此它会自动将ProductId
作为外键。如果要定义同一实体的多个集合,则必须手动定义外键。
public class Price
{
public int PriceId {get; set;}
public int ProductPurchaseId {get; set;}
public Product ProductPurchase {get; set;}
public int ProductRetailId {get; set;}
public Product ProductRetail {get; set;}
public DateTime ActivationDate {get; set;}
public decimal value {get; set;}
}
在流畅的 API 中:
modelBuilder<Product>().HasMany(p => p.PurchasePrices)
.WithRequired(p => p.ProductPurchase)
.HasForeignKey(p => p.ProductPurchaseId);
modelBuilder<Product>().HasMany(p => p.RetailPrices)
.WithRequired(p => p.ProductRetail)
.HasForeignKey(p => p.ProductRetailId);
这当然意味着您需要在Price
表中Product
2 个外键。
根据我对你要求的理解,你需要在你的价格表中有一个额外的字段,它会告诉你你要存储什么类型的价格。例如:
public class Price
{
public int PriceId {get; set;}
// The PriceType will recognise among different type of price- Sell Price, Purchase Price etc.
public string PriceType{get;set;}
// foreign key to Product:
public int ProductId {get; set;}
public Product Product {get; set;}
public DateTime ActivationDate {get; set;}
public decimal value {get; set;}
}