真的不可能在EF中开箱即用(又名非黑客方式)更新子集合吗?

本文关键字:方式 黑客 更新 子集合 EF 不可能 真的 | 更新日期: 2023-09-27 18:17:00

假设你的实体中有这些类。

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,那么您必须插入,如果用户编辑现有的Child,那么您需要更新,如果用户删除Child,那么您必须删除。现在很明显,如果你使用下面的代码

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

它将无法检测Child内部的变化,因为EF无法检测导航属性内部的变化。

我已经问了这个问题4次了,得到了不同的答案。那么,是否有可能在不变得复杂的情况下做这些事情呢?这个问题可以通过在ParentChild之间分离用户界面来解决问题,但我不想这样做,因为将ChildParent合并在一个菜单中在商业应用程序开发中非常常见,并且更用户友好。

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

public ActionResult(ParentViewModel model)
{
    var parentFromDB = context.Parent.Get(model.ParentID);
    if (parentFromDB != null)
    {
        parentFromDB.Childs = model.Childs;
    }
    context.SaveChanges();
}

EF不能检测子节点内部的变化,而不能判断如何处理旧的子节点。例如,如果parentFromDB有3个孩子,我第一次从DB拉它,然后我删除第二和第三个孩子。然后我得到The relationship could not be changed because one or more of the foreign-key properties is non-nullable时保存。

我相信事情是这样的:无法更改关系,因为一个或多个外键属性不可为空

这让我回到了原点,因为在我的场景中,我不能只是从DB获取并更新条目并调用SaveChanges

真的不可能在EF中开箱即用(又名非黑客方式)更新子集合吗?

,因为EF无法检测导航属性

中的更改

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

当然EF会跟踪导航属性的变化。它跟踪附加到上下文的所有实体的属性和关联的变化。因此,对你问题的答案,现在肯定地说…

是否可以开箱更新EF中的子集合

…是:

唯一的事情是:你不做开箱

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

  • 从数据库中获取实体。
  • 修改其属性或添加/删除元素到其集合
  • 呼叫SaveChanges() .

。Ef跟踪变化,你从来没有设置实体State s显式。

然而,在一个断开连接的(n层)场景中,这变得更加复杂。我们对实体进行序列化和反序列化,因此不会有任何上下文跟踪它们的变化。如果我们想要在数据库中存储实体,那么现在我们的任务就是让EF知道这些更改。基本上有两种方法:

  • 手动设置状态,基于我们对实体的了解(比如:主键> 0意味着它们存在并且应该被更新)
  • 绘制状态:从数据库中检索实体,并将反序列化实体的更改重新应用于它们。
当涉及到关联时,我们总是必须绘制状态。我们必须从数据库中获取当前实体,并确定添加/删除了哪些子实体。没有办法从反序列化的对象图本身推断出这一点。

有多种方法可以减轻绘制状态这一无聊而复杂的任务,但这超出了本问题的范围。参考:

  • 更新整个聚合的通用存储库
  • GraphDiff

这是因为你做得很奇怪。

这需要延迟加载来获取子节点(显然要根据您的使用进行修改)

//得到父母

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

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

EmailMessage(parent)是父类,它没有或有多个EmailAttachment's(child's)

 [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();

        }
    }

做你的自动应用程序的东西…正如你在评论中所说的。

然后当你准备保存,你有它作为正确的类型,如一次代表实体(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();

我花了几个小时尝试不同的解决方案,以找到一些处理这个问题的体面方法。这个列表太长了,我不能在这里都写下来,但有几个是……

  • 更改父实体状态
  • 更改子实体状态
  • 附加和分离实体
  • 清除dbSet.Local以避免跟踪错误
  • 尝试在ChangeTracker中编写客户逻辑
  • 重写DB到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

如果您正在使用entityframeworkcore,它提供了dbSet.Update()方法,该方法负责对象树的任何级别的任何更新。如需参考,请查看此处的文档链接