在不少系統中,存在多對多關係的維護。以下圖:sql
這種多對多結構在數據庫中大部分有三個數據表,其中兩個主表,還有一個關聯表,關聯表至少兩個字段,即左表主鍵、右表主鍵。數據庫
如上圖,其中的Supplier表和Product是主業務表,ProductSupplier是關聯表,在一些複雜的業務系統中,這樣的關係實在是太多了。以前在沒有使用EF這類ORM框架的時候,能夠經過代碼來維護這樣的關聯關係,查詢的時候扔過去一個Left Join語句,把數據取出來拼湊一下就能夠了。app
如今大多使用EF做爲ORM工具,處理起來這種問題反而變得麻煩了。緣由就是多關聯表之間牽牽扯扯的外鍵關係,一不當心就會出現各類問題。本文將從建模開始演示這種操做,提供一個多對多關係維護的參考。也歡迎你們能提供一些更好的實現方式。框架
在EF中建模已知的兩種方式:dom
兩種不一樣的建模方式帶來徹底迥異的增刪改查方式,第一種在EF中直接進行多對多的處理。而第二種是把多對多的關係處理間接的修改成了兩個一對多關係處理。async
在本文中重點介紹第一個多對多的狀況,第二個處理方式能夠參考Microsoft Identity代碼中,關於用戶角色的代碼。ide
說了好多廢話,下面正文。代碼環境爲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"); }); } }
只是作一個下操做展現,儘可能展現核心代碼,不作多餘的點綴了ui
使用VS的MVC腳手架,右鍵添加Controller,使用包含視圖的MVC 5控制器(使用Entity Framework),模型類選擇Product,一樣操做爲Supplier添加Controller。this
多對多關係新增分兩種狀況:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "ProductID,ProductName")] Product product) { //左右側都爲新增 if (ModelState.IsValid) { //使用代碼模擬新增右側表 var supplier = new List<Supplier> { new Supplier { SupplierName = "後臺新增供應商"+new Random(Guid.NewGuid().GetHashCode()).Next(1,100) }, new Supplier { SupplierName = "後臺新增供應商"+new Random(Guid.NewGuid().GetHashCode()).Next(1,100) }, }; //左右側表創建關聯關係 supplier.ForEach(s => product.Suppliers.Add(s)); //將左側表添加到數據上下文 db.Products.Add(product); //保存 db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
這裏直接在後臺模擬了新增產品和產品供應商的操做,當數據保存後,會在三個表中分別生成數據,以下:
可見這種新增的時候是不須要進行特別的處理
//POST: Products/Create //爲了防止「過多發佈」攻擊,請啓用要綁定到的特定屬性,有關 //詳細信息,請參閱 https://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create1([Bind(Include = "ProductID,ProductName")] Product product) { //左側新增數據,右側爲已存在數據 if (ModelState.IsValid) { //在數據庫中隨機取出兩個供應商 var dbSuppliers = db.Suppliers.OrderBy(s => Guid.NewGuid()).Take(2).ToList(); //爲產品添加供應商,創建與供應商之間的關聯 dbSuppliers.ForEach(s => { product.Suppliers.Add(s); // //由於EF有跟蹤狀態,因此無須添加狀態也能夠正常保存 // //db.Entry<Supplier>(s).State = System.Data.Entity.EntityState.Unchanged; }); //添加產品記錄到數據上下文 db.Products.Add(product); //執行保存 db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
咱們經過在後臺獲取第一個和最後一個供應商,而後模擬新增產品選擇以有供應商的用戶行爲。在數據庫中會添加一條產品記錄,兩條產品供應商關聯數據。以下:
看起來也沒什麼問題麼。so easy 啊。
注意:實際上咱們在開發中基本不會像如今這樣處理,執行編輯操做時實際流程是
看似簡單,這裏還要注意另一件事情,就是在操做過程當中,咱們是要進行數據對象的轉換的,這個轉換過程簡單歸納就是 Entity→Dto→(View Model→Dto→)Entity,因此咱們看看實際狀況下會碰到什麼問題
使用以下代碼替換Create的Post方法
//POST: Products/Create //爲了防止「過多發佈」攻擊,請啓用要綁定到的特定屬性,有關 //詳細信息,請參閱 https://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create2([Bind(Include = "ProductID,ProductName")] Product product) { //左側新增數據,右側爲已存在數據 if (ModelState.IsValid) { //模擬數據庫中取出數據 var dbSuppliers = db.Suppliers.OrderBy(s => Guid.NewGuid()).Take(2).AsNoTracking().ToList(); //加載右側表數據,從中選擇兩個做爲本次修改的關聯對象,Entity→Dto(model)轉換,轉換過程當中,Entity丟失了EF的狀態跟蹤 var suppliers = dbSuppliers.Select(s => new Supplier { SupplierID = s.SupplierID }).ToList(); //保存修改後的實體,Dto(model)→Entity轉換,一般頁面只回傳右表的主鍵Id suppliers.ForEach(s => { product.Suppliers.Add(s); }); db.Products.Add(product); db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
這個代碼執行後結果以下:
在上面的代碼執行完成之後,EF把右側表也作了新增處理,因此就出現右側添加了空數據的問題。
修改代碼:
//POST: Products/Create //爲了防止「過多發佈」攻擊,請啓用要綁定到的特定屬性,有關 //詳細信息,請參閱 https://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create2([Bind(Include = "ProductID,ProductName")] Product product) { //左側新增數據,右側爲已存在數據 if (ModelState.IsValid) { //.AsNoTracking() 不添加的時候,保存也報錯 var dbSuppliers = db.Suppliers.OrderBy(s => Guid.NewGuid()).AsNoTracking().Take(2).ToList(); //加載右側表數據,從中選擇兩個做爲本次修改的關聯對象,Entity→Dto(model)轉換,轉換過程當中,Entity丟失了EF的狀態跟蹤 var suppliers = dbSuppliers.Select(s => new Supplier { SupplierID = s.SupplierID }).ToList(); //保存修改後的實體,Dto(model)→Entity轉換,一般頁面只回傳右表的主鍵Id 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); }
執行新增操做後結果:
以上終於獲取了正常結果。上面兩處高亮代碼,下方修改狀態的是新增的代碼。咱們作個小實驗,把AsNoTracking()去掉看看會怎麼樣。
沒錯,直接報錯了。
System.InvalidOperationException HResult=0x80131509 Message=Attaching an entity of type 'Many2Many.Models.Supplier' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate. Source=EntityFramework StackTrace: 在 System.Data.Entity.Core.Objects.ObjectContext.VerifyRootForAdd(Boolean doAttach, String entitySetName, IEntityWrapper wrappedEntity, EntityEntry existingEntry, EntitySet& entitySet, Boolean& isNoOperation) 在 System.Data.Entity.Core.Objects.ObjectContext.AttachTo(String entitySetName, Object entity) 在 System.Data.Entity.Internal.Linq.InternalSet`1.<>c__DisplayClassa.<Attach>b__9() 在 System.Data.Entity.Internal.Linq.InternalSet`1.ActOnSet(Action action, EntityState newState, Object entity, String methodName) 在 System.Data.Entity.Internal.Linq.InternalSet`1.Attach(Object entity) 在 System.Data.Entity.Internal.InternalEntityEntry.set_State(EntityState value) 在 System.Data.Entity.Infrastructure.DbEntityEntry`1.set_State(EntityState value) 在 Many2Many.Controllers.ProductsController.<>c__DisplayClass8_0.<Create2>b__1(Supplier s) 在 E:\Github\Many2Many\Controllers\ProductsController.cs 中: 第 178 行 在 System.Collections.Generic.List`1.ForEach(Action`1 action) 在 Many2Many.Controllers.ProductsController.Create2(Product product) 在 E:\Github\Many2Many\Controllers\ProductsController.cs 中: 第 175 行 在 System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) 在 System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) 在 System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) 在 System.Web.Mvc.Async.AsyncControllerActionInvoker.<BeginInvokeSynchronousActionMethod>b__39(IAsyncResult asyncResult, ActionInvocation innerInvokeState) 在 System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`2.CallEndDelegate(IAsyncResult asyncResult) 在 System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResultBase`1.End() 在 System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethod(IAsyncResult asyncResult) 在 System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<InvokeActionMethodFilterAsynchronouslyRecursive>b__3d() 在 System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<>c__DisplayClass46.<InvokeActionMethodFilterAsynchronouslyRecursive>b__3f()
看錯誤堆棧信息是否是很熟悉?說出來可能不信,我曾經被這個問題折磨了一天~ 其實就是由於EF有實體跟蹤機制,不少時候問題就出在這裏,對EF的機制若是不瞭解的話很容易碰到問題。
一樣會產生錯誤的代碼還有以下:
//POST: Products/Create //爲了防止「過多發佈」攻擊,請啓用要綁定到的特定屬性,有關 //詳細信息,請參閱 https://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create2([Bind(Include = "ProductID,ProductName")] Product product) { //左側新增數據,右側爲已存在數據 if (ModelState.IsValid) { //.AsNoTracking() 不添加的時候,保存也報錯 //var dbSuppliers = db.Suppliers.OrderBy(s => Guid.NewGuid()).AsNoTracking().Take(2).ToList(); var dbSuppliers = db.Suppliers.OrderBy(s => Guid.NewGuid()).Take(2).ToList(); //加載右側表數據,從中選擇兩個做爲本次修改的關聯對象,Entity→Dto(model)轉換,轉換過程當中,Entity丟失了EF的狀態跟蹤 var suppliers = dbSuppliers.Select(s => new Supplier { SupplierID = s.SupplierID }).ToList(); //保存修改後的實體,Dto(model)→Entity轉換,一般頁面只回傳右表的主鍵Id suppliers.ForEach(item => { product.Suppliers.Add(item); //把這一行代碼踢出去執行,會有奇效 //db.Entry<Supplier>(item).State = System.Data.Entity.EntityState.Unchanged; }); db.Products.Add(product); //在這裏進行狀態設置 foreach (var item in product.Suppliers) { db.Entry<Supplier>(item).State = System.Data.Entity.EntityState.Unchanged; } db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
-咱們只是調整了一下修改右側表狀態的時機,EF很是機智的換了個錯誤提示方式!
錯誤信息以下:堆棧跟蹤信息:
System.InvalidOperationException HResult=0x80131509 Message=Saving or accepting changes failed because more than one entity of type 'Many2Many.Models.Supplier' have the same primary key value. Ensure that explicitly set primary key values are unique. Ensure that database-generated primary keys are configured correctly in the database and in the Entity Framework model. Use the Entity Designer for Database First/Model First configuration. Use the 'HasDatabaseGeneratedOption" fluent API or 'DatabaseGeneratedAttribute' for Code First configuration. Source=EntityFramework StackTrace: 在 System.Data.Entity.Core.Objects.ObjectStateManager.FixupKey(EntityEntry entry) 在 System.Data.Entity.Core.Objects.EntityEntry.AcceptChanges() 在 System.Data.Entity.Core.Objects.EntityEntry.ChangeObjectState(EntityState requestedState) 在 System.Data.Entity.Core.Objects.EntityEntry.ChangeState(EntityState state) 在 System.Data.Entity.Internal.StateEntryAdapter.ChangeState(EntityState state) 在 System.Data.Entity.Internal.InternalEntityEntry.set_State(EntityState value) 在 System.Data.Entity.Infrastructure.DbEntityEntry`1.set_State(EntityState value) 在 Many2Many.Controllers.ProductsController.Create2(Product product) 在 E:\Github\Many2Many\Controllers\ProductsController.cs 中: 第 219 行 在 System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) 在 System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) 在 System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) 在 System.Web.Mvc.Async.AsyncControllerActionInvoker.<BeginInvokeSynchronousActionMethod>b__39(IAsyncResult asyncResult, ActionInvocation innerInvokeState) 在 System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`2.CallEndDelegate(IAsyncResult asyncResult) 在 System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResultBase`1.End() 在 System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethod(IAsyncResult asyncResult) 在 System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<InvokeActionMethodFilterAsynchronouslyRecursive>b__3d() 在 System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<>c__DisplayClass46.<InvokeActionMethodFilterAsynchronouslyRecursive>b__3f()
以上兩個錯誤信息的實際產生緣由都是由於EF的實體跟蹤機制致使的。若是碰到相似問題,檢查你的實體是否是狀態很少。
使用第一個新增方法在增長一條數據,以區別現有數據,而後修改Edit 的Post方法:
// POST: Products/Edit/5 // 爲了防止「過多發佈」攻擊,請啓用要綁定到的特定屬性,有關 // 詳細信息,請參閱 https://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "ProductID,ProductName,SuppliersId")] Product product) { if (ModelState.IsValid) { var entity = db.Entry(product); entity.State = EntityState.Modified; entity.Collection(s => s.Suppliers).Load(); //不能像Identity中同樣,先clear在add,須要區別對待 if (product.SuppliersId.Any()) { var newList = new List<Supplier>(); Array.ForEach(product.SuppliersId, s => { newList.Add(new Supplier { SupplierID = s }); }); //須要移除的關係 var removeRelation = product.Suppliers.Except(newList, new SupplierComparer()).ToList(); //新增的關係 var addRelation = newList.Except(product.Suppliers, new SupplierComparer()).ToList(); removeRelation.ForEach(item => product.Suppliers.Remove(item)); addRelation.ForEach(item => { product.Suppliers.Add(item); db.Entry(item).State = EntityState.Unchanged; }); } db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
修改前數據以下:
修改後數據以下:
在修改的時候實際上是執行了三個操做
Entity Framework算是比較強大的ORM框架了,在使用過程當中一樣的需求可能有不一樣的實現方式,簡單的CRUD操做實現起來都很簡單了。在多對多的關係處理中,經過通用的倉儲類基本無法處理,通常要單獨實現,上文總結了經常使用的集中實現方式。