EF 代码优先:对同一集合类型进行一对多两次

EF code first: one-to-many twice to same collection type

简化:在我的数据库中,我有一种产品在不同日期以不同价格出售。换句话说,它有一个价格历史。我有两个 类:ProductPrice 具有一对多关系:

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; }
}

到目前为止一切顺利,Entity Framework 知道如何处理它。通过使用这两个 类 我可以获得特定日期特定产品的价格。

但是,如果我的产品有两个价格历史记录:购买价格历史记录和零售价格历史记录怎么办?

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.

如何操作?

由于 EF 命名约定,目前您的代码正在运行:

Code First infers that a property is a primary key if a property on a class is named “ID” (not case sensitive), or the class name followed by "ID". If the type of the primary key property is numeric or GUID it will be configured as an identity column.

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 table.

中有 2 个指向 Product 的外键

根据我对您的要求的理解,您需要在价格中添加一个额外的字段 table,它会告诉您要存储的价格类型。例如:

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;}
}

阅读有关 one-to-one foreign key associations 的故事并将其用于一对多关联后,我能够根据以下要求实现它:

  • 我可以有许多不同的 classes 和相同类型的 属性 T
  • 所有类型 T 的实例都可以放在一个 table 中,即使此类型的 "owner" 在不同的 table 中。
    • 一个class甚至可以有两个类型为T的属性。

例如:客户可能有 BillingAddress 和 DeliveryAddress,两者都是地址类型。两个地址可以放在一个table: Address.

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);
    }
}

此解决方案的聪明之处在于地址中没有 "owning" 用户。因此,如果我用一个地址定义一个新的 class,这个地址可以添加到相同的地址 table。所以如果我有十个不同的 classes 都有一个地址我不需要十个地址 tables.

如果你有一堆地址怎么办?

通常在一对多关系中,多方需要一方的外键加上对 "owner":

的引用

一个常见的例子:博客和 posts:一个博客有很多 posts。一个 post 正好属于一个博客:

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; }

并在 OnModelCreating 中:

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);
    }
}

我已经用其他 class 具有多个价格历史的系统对其进行了测试: - 所有价格将合二为一 table - 所有价格历史都将集中在一个 table - 每个对价格历史的引用都需要一个 priceHistoryId。

如果你仔细观察结果,它实际上是多对多关系的实现,其中价格历史是耦合 table。

我试图删除 PriceHistory class,并让一个产品在 OnModelCreating 中有多个价格集合和多对多集合,但这会导致 "Map" 语句魔术字符串,并为每个 PriceHistory 单独 tables。

Link to explanation about how to implement many-to-many