EF开箱即用(即非hacky方式)是否真的无法更新子集合?

19

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

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

由于EF无法检测导航属性内部的更改,因此它将无法检测到Child内部的更改。

我已经问了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个子项,然后我删除第2个和第3个子项。然后,在保存时出现由于一个或多个外键属性不可为空,因此无法更改关系

我认为这就是发生的事情: 由于一个或多个外键属性不可为空,因此无法更改关系

这让我重新回到起点,因为在我的情况下,我不能仅从数据库中获取并更新条目,然后调用SaveChanges


不确定我是否误解了您的问题。禁用更改跟踪是先决条件吗?否则,使用EF更改跟踪很容易实现。使用更改跟踪,您无需显式设置实体的状态,EF会为您完成,因此您对Childs集合进行的任何修改,包括集合中实体的修改,在提交上下文(SaveChanges)时都将自动包含在更改集中。 - odyss-jii
我同意odyss-jii的评论,但想要补充一些信息:当你在检索和更改属性之间保持上下文开放时,这只有在这种情况下才有效。否则,你将不得不自己设置实体状态,为此你必须首先获取上下文中的所有内容,因此你必须为每个对象创建一个条目并设置其状态。 - DevilSuichiro
1
类似的问题在Stack Overflow上:https://dev59.com/Po_ea4cB1Zd3GeqPTM5Thttps://dev59.com/HGMl5IYBdhLWcg3w3aPjhttps://dev59.com/uYTba4cB1Zd3GeqP3kSC - tickwave
以下是描述功能的代码片段:var obj=_dbcontext.Parent.Include(x=>x.Childs).FirstOrDefault(); obj.childs.FirstOrDefault().Property=1; 这将自动设置此子对象的实体状态为Modified,并在调用SaveChanges()时将更改写回数据库。 - DevilSuichiro
@DevilSuichiro:你能发完整的代码吗,这样我就可以将其标记为答案了吗?假设我有一个父级(P1),有两个子级(C1和C2)。然后我删除C1,修改C2,并添加C3和C4。所以我的父级现在有三个子级(C2,C3和C4),EF应该从数据库中删除C1,更新C2,并将C3和C4添加到数据库中。 - tickwave
显示剩余2条评论
4个回答

14
因为EF无法检测导航属性内的更改。
这似乎是对事实的某种扭曲描述,即_dbContext.Entry(obj).State = EntityState.Modified不会将导航属性标记为已修改。
当然,EF跟踪导航属性的更改。它跟踪上下文附加的所有实体的属性和关联的更改。因此,您的问题的答案现在肯定是...
“是否可以直接更新EF中的子集合”
...是:是。
唯一的问题是:你不使用 "out of the box" 的方法。
任何实体的“out of the box”更新方式,无论是父级还是某个集合中的孩子,如下所示:
从数据库获取实体。 修改它们的属性或添加/删除其集合的元素 调用SaveChanges()
这就是全部。EF跟踪更改,您从未显式设置实体State
但是,在断开连接的(n层)场景中,情况变得更加复杂。我们会序列化和反序列化实体,因此不能有任何跟踪更改的上下文。如果我们想将实体存储在数据库中,现在我们的任务是让EF知道更改。基本上有两种方法:
根据我们对实体了解的内容手动设置状态(例如,主键> 0表示它们存在并且应更新) “绘制状态”:从数据库检索实体,并将反序列化的实体中的更改重新应用于它们。
当涉及到关联时,我们必须始终“绘制状态”。我们必须从数据库中获取当前实体,并确定哪些子项已添加/删除。无法从反序列化的对象图本身推断出这一点。

有多种方法可以缓解沉闷而繁琐的状态绘制任务,但这超出了此问答的范围。一些参考资料:


1

这是因为你的操作方式有些奇怪。

这需要使用懒加载来获取子元素(显然需要根据你的使用情况进行修改)

//获取父元素

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操作。

当您准备好保存并且已将其转换为正确的类型(即代表实体(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();

在我的情况下,我会从上下文中获取现有的EmailMessage以及其属性,即EmailAttachment,并使用AutoMapper将其映射到我的EmailMessageViewModel中(此ViewModel与EmailMessage具有1:1的属性)。之后,我将EmailMessageViewModel映射回一个新的EmailMessage对象。这个新的EmailMessage可能有修改/新增/删除的EmailAttachment。这时我不知道如何将其更新到上下文中。我的原始帖子:https://dev59.com/Po_ea4cB1Zd3GeqPTM5T - tickwave
简单,重新获取原始数据...并使用自动映射器更改被视为附加到上下文的实体(父级)的属性。然后将获取的实体的子项设置为新的子项。例如:Parent.Childs = 修改后的Childs。EF足够智能,可以为您解决所有问题...这就是为什么EF很棒的原因。 - Seabizkit
好的,但是新添加的附件或删除的附件怎么处理?到目前为止,我的处理方式如下:
  1. 获取原始数据
  2. EmailMessageViewModel进行比较
  3. 如果找到相同ID,则将实体状态设置为“Modified”;如果未找到,则设置为“Added”;如果在获取的版本中找到但在EmailMessageViewModel中没有找到,则将其设置为“Deleted”
这种方法可行吗?
- tickwave
defo 可以用;-) 我已经做了很多次。一旦你知道你能做什么和不能做什么,EF 就非常棒。 - Seabizkit
需要注意的是:这是我和其他几个人提到的相同功能,但是使用另一个数据库调用而不是通过对象树运行并相应地设置实体状态。如果您无法完全将对象树保留在上下文中,请选择您喜欢的方式。 - DevilSuichiro
显示剩余3条评论

1
我花了几个小时尝试不同的解决方案来找到一个处理这个问题的好方法。列表太长了,我无法在这里写出所有的解决方案,但其中有几个是...

  • 更改父实体状态
  • 更改子实体状态
  • 附加和分离实体
  • 清除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

1
问题明确提到删除现有的子项,但这个解决方案不能做到,对吗? - GuyB

0
如果您正在使用EntityFramework Core,它提供了dbSet.Update()方法,可以处理对象树中任何级别的更新。 请参考文档链接here

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接