c# - Entity Framework ,代码优先,如何将主键添加到多对多关系映射的表中?

标签 c# asp.net-mvc entity-framework ef-code-first many-to-many

我正在构建 ASP.NET MVC 5 应用程序。我正在使用 Entity Framework 6.1,代码优先的方法来生成数据库。 ProductCategory 之间存在多对多关系。

public class Product
{
    public long Id { get; set; }        
    public string Name { get; set; }
    public string Description { get; set; }

    // navigation
    public virtual ICollection<Category> Categories { get; set; }
}

public class Category
{
    public long Id { get; set; }        
    public string Name { get; set; }

    // navigation
    public virtual ICollection<Product> Products { get; set; }
}

Dbcontext 类中,我覆盖了 OnModelCreating 方法来为多对多关系创建表,如下所示:

modelBuilder.Entity<Product>().HasMany<Category>(s => s.Categories).WithMany(c => c.Products)
.Map(cs =>
{
    cs.MapLeftKey("ProductId");
    cs.MapRightKey("CategoryId");
    cs.ToTable("ProductCategories");
});

该表连接了两个外键。 如何向此联结表添加一个 ID(作为主键)?

ProductCategories

 - Id // add id as primary key
 - ProductId    
 - CategoryId

最佳答案

让我扩展@bubi 的回答:

默认情况下,当您定义多对多关系(使用属性或 FluentAPI)时,EF 会创建它(向数据库添加额外的表)并允许您将多个产品添加到一个类别以及将多个类别添加到一个产品。但它不允许您将链接表行作为实体访问

如果您需要这样的功能,例如您要以某种方式管理这些链接,例如将它们标记为“已删除”或设置“优先级”,您需要:

  1. 创建新实体 (ProductCategoryLink)
  2. 将它作为另一个 DbSet 添加到您的上下文中
  3. 相应地更新产品和类别实体中的关系。

对你来说它可能喜欢:

实体

public class Product
{
    [Key]
    public long ProductId { get; set; }        
    public string Name { get; set; }
    public string Description { get; set; }

    [InverseProperty("Product")]
    public ICollection<ProductCategoryLink> CategoriesLinks { get; set; }
}

public class Category
{
    [Key]
    public long CategoryId { get; set; }        
    public string Name { get; set; }

    [InverseProperty("Category")]
    public ICollection<ProductCategoryLink> ProductsLinks { get; set; }
}

public class ProductCategoryLink
{
    [Key]
    [Column(Order=0)]
    [ForeignKey("Product")]
    public long ProductId { get; set; }        

    public Product Product { get; set; }

    [Key]
    [Column(Order=1)]
    [ForeignKey("Category")]
    public long CategoryId { get; set; }       

    public Category Category { get; set; }
}

我使用属性方式来定义关系,因为我更喜欢这种方式。但是您可以轻松地用具有两个一对多关系的 FluentAPI 替换它:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{

   // Product to Links one-to-many 
   modelBuilder.Entity<ProductCategoryLink>()
                    .HasRequired<Product>(pcl => pcl.Product)
                    .WithMany(s => s.CategoriesLinks)
                    .HasForeignKey(s => s.ProductId);


   // Categories to Links one-to-many 
   modelBuilder.Entity<ProductCategoryLink>()
                    .HasRequired<Category>(pcl => pcl.Category)
                    .WithMany(s => s.ProductsLinks)
                    .HasForeignKey(s => s.CategoryId);
}

上下文

这不是必需的,但很可能您需要将链接直接保存到上下文,所以让我们为它们定义一个 DbSet。

public class MyContext : DbContext 
{
    public DbSet<Product> Products { get; set; }

    public DbSet<Category > Categories{ get; set; }

    public DbSet<ProductCategoryLink> ProductCategoriesLinks { get; set; }    
}

两种实现方式

我使用属性来定义关系的另一个原因是它表明(都标有 [Key] 属性(还要注意 [Column(Order=X)] 属性]))ProductCategoriesLink 实体中的两个 FK 成为一个组合PK,因此您无需定义另一个属性(如“ProductCategoryLinkId”)并将其标记为特殊的 PK 字段。

您总能找到所需的链接实体,您所需要的只是两个主键:

using(var context = new MyContext)
{
    var link = context.ProductCategoriesLinks.FirstOrDefault(pcl => pcl.ProductId == 1 
                                                                 && pcl.CategoryId == 2);
}

此外,这种方法限制了保存具有相同产品和类别的多个链接的任何机会,因为它们是复杂的键。如果您更喜欢 Id 与 FK 分开的方式,则需要手动添加 UNIQUE 约束。

无论您选择哪种方式,您都将达到根据需要操作链接并在需要时向它们添加其他属性的目标。

注一

因为我们将多对多链接定义为单独的实体产品和类别,彼此之间不再有直接关系。所以你需要更新你的代码:

现在您需要定义一个 ProductCategoryLink 实体并根据您的逻辑上下文使用以下三种方式之一保存它,而不是直接将产品添加到类别或直接将类别添加到产品:

public void AddProductToCategory(Product product, Company company)
{
    using (var context = new MyContext())
    {
         // create link
         var link = new ProductCategoryLink{
             ProductId = product.ProductId, // you can leave one link
             Product = product,             // from these two
             CategoryId = category.CategoryId, // and the same here
             Category = category
             };

         // save it
         // 1) Add to table directly - most general way, because you could 
         // have only Ids of product and category, but not the instances
         context.ProductCategoriesList.Add(link);

         // 2) Add link to Product - you'll need a product instance
         product.CategoriesLinks.Add(link);

         // 3) Add link to Category - you'll need a category instance
         category.ProductLinks.Add(link);

         context.SaveChanges();
    }
}

注2

还请记住,如果您需要查询第二个链接实体,您需要 .Include() 它:

public IEnumerable<Product> GetCategoryProducts(long categoryId)
{
    using (var context = new MyContext())
    {
         var products = context.Categories
                               .Include(c => c.ProductsCategoriesLinks.Select(pcl => pcl.Product))
                               .FirstOrDefault(c => c.CategoryId == categoryId);

         return products;
    }
}

更新:

关于 SO 有一个相同的问题和详细的答案: Create code first, many to many, with additional fields in association table

关于c# - Entity Framework ,代码优先,如何将主键添加到多对多关系映射的表中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32006681/

相关文章:

c# - ASP.NET MVC 5 身份 userManager.IsInRole

c# - 尽管将 PreserveReferencesHandling 设置为 "None",Json.Net 仍将 $id 添加到 EF 对象

c# - 如何使 WPF DataGrid NewItemPlaceholder 始终可见

c# - .net 不可变对象(immutable对象)

c# - 如何断言顺序不重要的 IEnumerable 参数?

c# - WPF/XAML 将元素的宽度绑定(bind)到屏幕大小的一部分

asp.net-mvc - 在 MVC3 应用程序中结合 UIhint 和附加元数据?

c# - 对 ASP.NET MVC Controller 的 Ajax POST 调用给出 net::ERR_CONNECTION_RESET

c# - 在 EntityFramework 4.0 中处理 DataContext

c# - LINQ to Entities 如何更新记录