简体   繁体   中英

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

Simplified: In my DB I have a product that was sold with different prices on different dates. In other words it has a Price History . I have two classes: Product and Price with a one-to-many relationship:

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

So far so good, Entity Framework knows how to handle this. With the use of these two classes I am able to get the price of a certain product on a certain date.

But what if my product has two price histories: a Purchase Price history and a Retail Price History?

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

Because these two collections are to the same type I don't want separate tables filled with object of the same type (the real reason: I have a lot of classes with price collections).

So I have to do a bit of coding using Fluent API. My gut feeling says I need joining tables, like in a many-to-many relationship, mayby using the ManyToManyNavigationPropertyConfiguration.Map.

How to do this?

At the moment your code is working because of the EF naming convention:

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 sees you have a one-to-many so it automatically takes ProductId as the foreign key. If you want to define multiple collections of the same entity you'll have to define your foreign keys manually.

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

And in fluent 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);

This of course means you need to have 2 foreign keys to Product in your Price table.

As per my understanding of your requirement you need one extra field in your price table which will tell you what type of price you are going to store. For example:

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

After reading a story about one-to-one foreign key associations and using this for a one-to-many association I was able to implement it with the following requirements:

  • I can have many different classes with a property of the same type T
  • All instances type T can be put in one table, even though the "owner" of this type is in different tables.
    • A class can even have two properties of type T.

For instance: A customer may have a BillingAddress and a DeliveryAddress, both of type Address. Both addresses can be put in one 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);
    }
}

The smart thing in this solution is that the Address does not have an "owning" user in it. So if I define a new class with an Address this address can be added to the same table of Address. So If I have ten different classes that all have an address I don't need ten address tables.

What to do if you have a collection of addresses?

Normally in a one-to-many relation the many side needs a foreign key to the one side plus a reference to the "owner":

An often seen example: blogs and posts: one blog has many posts. One post belongs to exactly one blog:

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

This naming will automatically lead to the correct one-to-many relationship, but if you want to specify in the DbContext:

public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

and in OnModelCreating:

modelBuilder.Entity<Blog>()
    .HasMany(b => b.Posts)
    .WithRequired(post => post.Blog)
    .HasForeignKey(post => post.BlogId);

Even if you would not need Post.Blog, you can't remove this property, because of the model creating. If you would remove it you would end up with magic strings to define the foreign key.

To be able to also have a collection of addresses (or in my original question: a lot of price histories, where each price history is a collection of prices) I combined these two methods.

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

I've tested it with other classes that have several price histories: - All prices will be in one table - All price histories will be in one table - Each reference to a price history needs a priceHistoryId.

If you look closely to the result it is in fact the implementation of a many-to-many relation where the price history is the coupling table.

I've tried to remove the PriceHistory class, and let a Product have several collections of Prices with a many-to-many in OnModelCreating, but that would lead to "Map" statements with magic strings, and separate tables for each PriceHistory.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM