真的不可能在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次了,得到了不同的答案。那么,是否有可能在不变得复杂的情况下做这些事情呢?这个问题可以通过在Parent
和Child
之间分离用户界面来解决问题,但我不想这样做,因为将Child
和Parent
合并在一个菜单中在商业应用程序开发中非常常见,并且更用户友好。
更新:我正在尝试下面的解决方案,但它不工作。
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无法检测导航属性
中的更改
这似乎是对_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()方法,该方法负责对象树的任何级别的任何更新。如需参考,请查看此处的文档链接