是否真的不可能在EF中更新子集合(也称为非hacky方式)?

时间:2015-10-10 16:51:44

标签: c# entity-framework entity-framework-6

我们假设你的实体中有这些课程。

public class Parent
{
    public int ParentID { get; set; }
    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int ChildID { get; set; }
    public int ParentID { get; set; }
    public virtual Parent Parent { get; set; }
}

您有一个用户界面来更新Parent及其Children,这意味着如果用户添加新Child,则必须插入,如果用户编辑现有{ {1}}然后您需要更新,如果用户删除Child,则必须删除。现在很明显,如果你使用以下代码

Child

它无法检测到public void Update(Parent obj) { _parent.Attach(obj); _dbContext.Entry(obj).State = EntityState.Modified; _dbContext.SaveChanges(); } 内的更改,因为EF无法检测到导航属性中的更改。

我一直在问这个问题4次,得到的答案很复杂。那么实际上可以做到这一点而不会变得复杂吗?此问题可以通过在ChildParent之间分隔用户界面来解决问题,但我不想,因为在一个菜单中合并ChildChild是在业务应用程序开发中非常常见,并且更加用户友好。

更新: 我正在尝试下面的解决方案,但它不起作用。

Parent

EF没有检测到儿童内部的变化,而是无法告诉孩子如何处理。例如,如果public ActionResult(ParentViewModel model) { var parentFromDB = context.Parent.Get(model.ParentID); if (parentFromDB != null) { parentFromDB.Childs = model.Childs; } context.SaveChanges(); } 第一次从DB中提取3个孩子,那么我会删除第2个和第3个孩子。然后我在保存时获得parentFromDB

我相信这就是发生的事情: The relationship could not be changed because one or more of the foreign-key properties is non-nullable

这让我回到原点,因为在我的场景中,我无法从数据库中获取并更新条目并致电The relationship could not be changed because one or more of the foreign-key properties is non-nullable

4 个答案:

答案 0 :(得分:10)

  

因为EF无法检测导航属性中的更改

这似乎是对_dbContext.Entry(obj).State = EntityState.Modified未将导航属性标记为已修改的事实的一种有点扭曲的描述。

当然EF跟踪导航属性的变化。它跟踪附加到上下文的所有实体的属性和关联的更改。因此,你的问题的答案,现在已经积极陈述......

  

是否可以立即更新EF中的子集合

...是:

唯一的事情是:你没有开箱即用

更新任何实体的“开箱即用”方式,无论是父项还是某个集合中的子项,都是:

  • 从数据库中获取实体。
  • 修改其属性或向其馆藏添加/删除元素
  • 致电SaveChanges()

这就是全部。如果跟踪更改,您永远不会明确设置实体State

但是,在断开连接(n层)的情况下,这会变得更加复杂。我们序列化和反序列化实体,因此不能有任何跟踪其更改的上下文。如果我们想将实体存储在数据库中,那么现在我们的任务就是让EF知道这些变化。基本上有两种方法可以做到这一点:

  • 根据我们对实体的了解手动设置状态(例如:主键&gt; 0表示它们存在且应该更新)
  • 绘制状态:从数据库中检索实体,然后将反序列化实体的更改重新应用于它们。

当涉及到关联时,我们总是必须绘制状态。我们必须从数据库中获取当前实体,并确定添加/删除了哪些子项。没有办法从反序列化的对象图本身推断出这一点。

有各种方法来缓解这种无聊和精心绘制国家的任务,但这超出了本Q&amp; A的范围。一些参考文献:

答案 1 :(得分:1)

我已经花费了数小时尝试不同的解决方案,以找到解决该问题的一些不错的方法。列表太长了,我无法在这里全部写出来,但是很少...

  • 更改父实体状态
  • 更改子实体状态
  • 连接和分离实体
  • 清除dbSet.Local以避免跟踪错误
  • 试图在ChangeTracker中编写客户逻辑
  • 重写数据库到View模型之间的映射逻辑
  • ....依此类推....

什么都没有做,但是最后,这是只需很小的改动就能解决整个问题的方法

使用此解决方案,您需要手动停止设置状态。只需调用一次dbSet.Update()方法,EF就会负责内部状态管理。

注意::即使您正在使用实体的分离图,甚至使用具有嵌套父子关系的实体,此方法仍然有效。

代码前

public void Update(Parent obj)
{
    _parent.Attach(obj);
    _dbContext.Entry(obj).State = EntityState.Modified;
    _dbContext.SaveChanges();
}

代码后:

public void Update(Parent obj)
{
    dbSet.Update(obj);
    _dbContext.SaveChanges();
}

参考:https://www.learnentityframeworkcore.com/dbset/modifying-data#:~:text=DbSet%20Update&text=The%20DbSet%20class%20provides,with%20individual%20or%20multiple%20entities.&text=This%20method%20results%20in%20the,by%20the%20context%20as%20Modified%20

答案 2 :(得分:0)

它让你很奇怪。

这需要延迟加载以获取孩子(显然会根据您的使用情况进行修改)

//得到父母

var parent = context.Parent.Where(x => x.Id == parentId).SingleOrDefault();

为您编写了一整套测试方法。 (适用于您的情况)

EmailMessage(父级)是父级,它没有或很多EmailAttachment(子级)

 [TestMethod]
    public void TestMethodParentChild()
    {
        using (var context = new MyContext())
        {
            //put some data in the Db which is linked
            //---------------------------------
            var emailMessage = new EmailMessage
            {
                FromEmailAddress = "sss",
                Message = "test",
                Content = "hiehdue",
                ReceivedDateTime = DateTime.Now,
                CreateOn = DateTime.Now
            };
            var emailAttachment = new EmailAttachment
            {
                EmailMessageId = 123,
                OrginalFileName = "samefilename",
                ContentLength = 3,
                File = new byte[123]
            };
            emailMessage.EmailAttachments.Add(emailAttachment);
            context.EmailMessages.Add(emailMessage);
            context.SaveChanges();
            //---------------------------------


            var firstEmail = context.EmailMessages.FirstOrDefault(x => x.Content == "hiehdue");
            if (firstEmail != null)
            {
                //change the parent if you want

                //foreach child change if you want
                foreach (var item in firstEmail.EmailAttachments)
                {
                    item.OrginalFileName = "I am the shit";
                }
            }
            context.SaveChanges();


        }
    }

更新

按照你在评论中的说法做你的AutoMappper Stuff ......

然后当你准备保存并将它作为正确的类型返回时,即一次代表实体(Db)然后执行此操作。

var modelParent= "Some auto mapper magic to get back to Db types."

var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
//use automapper here to update the parent again

if (parent != null)
{
  parent.Childs = modelParent.Childs;
}
//this will update all childs ie if its not in the new list from the return 
//it will automatically be deleted, if its new it will be added and if it
// exists it will be updated.
context.SaveChanges();

答案 3 :(得分:0)

如果您使用的是entityframework核心,它将提供dbSet.Update()方法,该方法可以处理任何级别的对象树中的任何更新。 作为参考,请检查文档链接here