問題描述 在很多系統中,存在多對多關係的維護。如下圖: 這種結構大部分有三個數據表,其中兩個具有主鍵的表,還有一個具有兩個鍵的關聯表,這個表中的兩個欄位既是主鍵又是外鍵。 如上圖,其中的Supplier表和Product是主業務表,ProductSupplier是關聯關係表,如果處理過一些複雜的業務... ...
問題描述
在很多系統中,存在多對多關係的維護。如下圖:
這種結構大部分有三個數據表,其中兩個具有主鍵的表,還有一個具有兩個鍵的關聯表,這個表中的兩個欄位既是主鍵又是外鍵。
如上圖,其中的Supplier表和Product是主業務表,ProductSupplier是關聯關係表,如果處理過一些複雜的業務系統,會發現這樣的關係實在是太多了。之前在沒有使用EF這類ORM框架的時候,可以通過代碼來維護這樣的關聯關係,查詢的時候扔過去一個Left Join語句,把數據取出來拼湊一下就可以了。
現在大多使用EF作為ORM工具,處理起來這種問題反而變得麻煩了,原因就是多關聯表之間牽牽扯扯的外鍵關係,一部小心就會出現各種問題。本文將從建模開始演示這種操作,提供一個多對多關係維護的參考。也歡迎大家能提供一些更好的實現方式。
在EF中建模
在EF中建模已知的兩種方式:
- 方式一,在數據上下文中添加兩個主實體類。使用Fluent Api配置在資料庫中生成其關聯表,但是在EF中不會體現。
- 方式二,在數據上下文中添加三個實體類,除了兩個主實體類外還包含第一個關聯表的定義,資料庫中存在三張表,EF數據上下文中對應三個實體。
兩種不同的建模方式帶來完全迥異的增刪改查方式,第一種在EF中直接進行多對多的處理。而第二種是把多對多的關係處理間接的修改為了兩個一對多關係處理。
在本文中重點介紹第一個多對多的情況,第二個處理方式可以參考Microsoft Identity代碼中,關於用戶角色的代碼。
說了好多廢話,下麵正文。代碼環境為VS 2017 ,MVC5+EF6 ,資料庫 SQL Server 2012 r2
方式一 實體定義代碼:
public class Product { public Product() { this.Suppliers = new List<Supplier>(); } [Display(Name = "Id")] public long ProductID { get; set; } [Display(Name = "產品名稱")] public string ProductName { get; set; } //navigation property to Supplier [Display(Name = "供應商")] public virtual ICollection<Supplier> Suppliers { get; set; } } public class Supplier { public Supplier() { this.Products = new List<Product>(); } [Display(Name = "Id")] public long SupplierID { get; set; } [Display(Name = "供應商名稱")] public string SupplierName { get; set; } [Display(Name = "提供產品")] // navigation property to Product public virtual ICollection<Product> Products { get; set; } }
數據上下文中,多對多關係配置:
public class MyDbContext : DbContext { public MyDbContext() : base("DefaultConnection") { Database.SetInitializer<MyDbContext>(null); } public DbSet<Product> Products { get; set; } public DbSet<Supplier> Suppliers { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Product>().HasMany(p => p.Suppliers).WithMany(s => s.Products) .Map(m => { m.MapLeftKey("ProductId"); m.MapRightKey("SupplierId"); m.ToTable("ProductSupplier"); }); } }
因為我們只是做一個下操作展示,儘量展示核心代碼,不做多餘的點綴了
右鍵添加Controller,使用包含視圖的MVC 5控制器(使用Entity Framework),模型類選擇Product,同樣操作為Supplier添加Controller。
Insert操作
多對多關係新增分兩種情況:
- 兩邊實體均為新增,常見的業務場景如,博客發文時同時添加新的標簽等。使用如下代碼覆蓋Create 動作的Post方法
// POST: Products/Create // 為了防止“過多發佈”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 https://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "ProductID,ProductName")] Product product) { if (ModelState.IsValid) { //雙表都為新增 var supplier = new List<Supplier> { new Supplier { SupplierName = "後臺演示,新增供應商1" }, new Supplier { SupplierName = "後臺演示,新增供應商2" }, }; supplier.ForEach(s => product.Suppliers.Add(s)); db.Products.Add(product); db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
這裡直接在後臺模擬了新增產品和產品供應商的操作,當數據保存後,會在三個表中分別生成數據,如下
可見這種新增的時候是不需要進行特別的處理
- 第二種情況,一邊實體為新增,另一邊實體為資料庫已存在,常見業務場景如,博客發文選擇已有分類時。使用如下代碼覆蓋Create 的Post方法。
//POST: Products/Create //為了防止“過多發佈”攻擊,請啟用要綁定到的特定屬性,有關 //詳細信息,請參閱 https://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "ProductID,ProductName")] Product product) { if (ModelState.IsValid) { //左側新增數據,右側為已存在數據 List<Supplier> suppliers = new List<Supplier> { db.Suppliers.OrderBy(s=>s.SupplierID).First(), db.Suppliers.OrderByDescending(s => s.SupplierID).First() }; //因為右側數據在資料庫中已經存在,所以需要在這裡把狀態設置為Unchanged,如果不設置關聯關係無法生成; suppliers.ForEach(s => { product.Suppliers.Add(s); db.Entry<Supplier>(s).State = System.Data.Entity.EntityState.Unchanged; }); db.Products.Add(product); db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
我們通過在後臺獲取第一個和最後一個供應商,然後模擬新增產品選擇以有供應商的用戶行為。在資料庫中會添加一條產品記錄,兩條產品供應商關聯數據。如下:
註意:高亮代碼,如果缺少這一行代碼,關聯關係是無法建立的,只會在資料庫中添加一條產品記錄。如果主鍵欄位非自增時,可能會報錯,無法插入重覆的主鍵 blah~blah~
Update操作
使用第一個新增方法在增加一條數據,以區別現有數據,然後修改Edit 的Post方法:
// POST: Products/Edit/5 // 為了防止“過多發佈”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 https://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "ProductID,ProductName")] Product product) { if (ModelState.IsValid) { var entity = db.Entry(product); entity.State = EntityState.Modified; entity.Collection(s => s.Suppliers).Load(); product.Suppliers.Clear(); //將現有表作為數據傳進去 List<Supplier> changeSuppliers = new List<Supplier> { db.Suppliers.OrderBy(s=>s.SupplierID).First(), db.Suppliers.OrderByDescending(s => s.SupplierID).First() }; changeSuppliers.ForEach(supplier => product.Suppliers.Add(supplier)); db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }修改前數據如下:
修改後數據如下:
在修改的時候其實是執行了三個操作
- 修改實體
- 刪除實體關聯關係
- 生成新的實體關聯關係
因為演示的環境中,主鍵是自增的,所以操作看起來挺簡單的。後面如果有空的話在演示一下非自增欄位的新增、更新操作。
有空再續~
dddd
dddd