Entity Framework 6 多对多增改操做指南

问题描述

在不少系统中,存在多对多关系的维护。以下图:sql

这种多对多结构在数据库中大部分有三个数据表,其中两个主表,还有一个关联表,关联表至少两个字段,即左表主键、右表主键。数据库

如上图,其中的Supplier表和Product是主业务表,ProductSupplier是关联表,在一些复杂的业务系统中,这样的关系实在是太多了。以前在没有使用EF这类ORM框架的时候,能够经过代码来维护这样的关联关系,查询的时候扔过去一个Left Join语句,把数据取出来拼凑一下就能够了。app

如今大多使用EF做为ORM工具,处理起来这种问题反而变得麻烦了。缘由就是多关联表之间牵牵扯扯的外键关系,一不当心就会出现各类问题。本文将从建模开始演示这种操做,提供一个多对多关系维护的参考。也欢迎你们能提供一些更好的实现方式。框架

在EF中建模

在EF中建模已知的两种方式:dom

  1. 方式一,在数据上下文中添加两个主实体类。使用Fluent Api配置在数据库中生成其关联表,可是在EF中不会体现。
  2. 方式二,在数据上下文中添加三个实体类,除了两个主实体类外还包含第一个关联表的定义,数据库中存在三张表,EF数据上下文中对应三个实体。

两种不一样的建模方式带来彻底迥异的增删改查方式,第一种在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

 

Insert操做

多对多关系新增分两种状况:

  1. 左右侧同时新增。使用以下代码覆盖Create 动做的Post方法
    [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);
            }

    这里直接在后台模拟了新增产品和产品供应商的操做,当数据保存后,会在三个表中分别生成数据,以下:

    image

    可见这种新增的时候是不须要进行特别的处理

  2. 左侧新增,关联存在右表数据。常见业务场景如,博客发文选择已有分类时。使用以下代码覆盖Create 的Post方法。
    //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);
            }

    咱们经过在后台获取第一个和最后一个供应商,而后模拟新增产品选择以有供应商的用户行为。在数据库中会添加一条产品记录,两条产品供应商关联数据。以下:

    image

    看起来也没什么问题么。so easy 啊。

    注意:实际上咱们在开发中基本不会像如今这样处理,执行编辑操做时实际流程是

    1. 进入编辑页面,获取要编辑的数据,在页面上展现
    2. 在页面上修改表单,创建与右侧表单关联关系(经过下拉框、多选操做、弹窗多选等)
    3. 提交表单,后台执行修改的保存动做

看似简单,这里还要注意另一件事情,就是在操做过程当中,咱们是要进行数据对象的转换的,这个转换过程简单归纳就是 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);
        }

这个代码执行后结果以下:

image

在上面的代码执行完成之后,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);
        }

执行新增操做后结果:

image

以上终于获取了正常结果。上面两处高亮代码,下方修改状态的是新增的代码。咱们作个小实验,把AsNoTracking()去掉看看会怎么样。

没错,直接报错了。

image

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很是机智的换了个错误提示方式!

错误信息以下:

image

堆栈跟踪信息:

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的实体跟踪机制致使的。若是碰到相似问题,检查你的实体是否是状态很少。

 

Update操做

使用第一个新增方法在增长一条数据,以区别现有数据,而后修改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);
        }

修改前数据以下:

 

image

修改后数据以下:

image

在修改的时候实际上是执行了三个操做

  1. 加载实体的关联关系
  2. 修改实体
  3. 移除实体关联关系 (多条sql)
  4. 添加新的实体关联关系 (多条sql)

 

Entity Framework算是比较强大的ORM框架了,在使用过程当中一样的需求可能有不一样的实现方式,简单的CRUD操做实现起来都很简单了。在多对多的关系处理中,经过通用的仓储类基本无法处理,通常要单独实现,上文总结了经常使用的集中实现方式。

相关文章
相关标签/搜索