当保存EF4 POCO对象的更改时更新关系

111

Entity Framework 4,POCO对象和ASP.Net MVC2。我有一个多对多的关系,比如BlogPost和Tag实体之间的关系。这意味着在我的T4生成的POCO BlogPost类中,我有:

public virtual ICollection<Tag> Tags {
    // getter and setter with the magic FixupCollection
}
private ICollection<Tag> _tags;

我想从ObjectContext的一个实例中获取BlogPost和相关标签,并将其发送到另一层(MVC应用程序中的View)。稍后,我会收到已更改属性和更改关系的更新后的BlogPost。例如,它具有标签“A”“B”和“C”,新标签为“C”和“D”。在我的特定示例中,没有新标签,标签的属性也从未更改,因此应保存的是更改的关系。现在,我需要将其保存在另一个ObjectContext中。(更新:现在我尝试在同一个上下文实例中执行并失败了。)
问题:我无法正确保存关系。我尝试了所有我能找到的方法:
- Controller.UpdateModel和Controller.TryUpdateModel不起作用。 - 从上下文中获取旧的BlogPost,然后修改集合不起作用。(使用来自下一个点的不同方法) - This可能有效,但我希望这只是一个解决方法,而不是解决方案:(。 - 尝试了每种可能组合的BlogPost和/或Tags的Attach/Add/ChangeObjectState函数。失败了。 - This看起来就是我需要的,但它不起作用(我尝试修复它,但对于我的问题无法做到)。 - 尝试更改上下文的关系对象的ChangeState/Add/Attach/...。失败了。
"Doesn't work"通常意味着我已经针对给定的“解决方案”进行了工作,直到它产生没有错误并至少保存了BlogPost的属性。与关系的发生情况各不相同:通常标签会再次添加到标签表中,并具有新的PK,保存的BlogPost引用这些标签而不是原始标签。当然,返回的标签具有PK,并且在保存/更新方法之前,我检查了PK,它们等于数据库中的PK,所以EF可能认为它们是新对象,并且这些PK是临时的。
我知道一个问题,可能会使找到自动简单的解决方案变得不可能:当POCO对象的集合发生更改时,应该通过上述提到的虚拟集合属性来完成,因为这样FixupCollection技巧将更新许多对多关系另一端的反向引用。但是,当视图“返回”已更新的BlogPost对象时,这种情况并未发生。这意味着也许没有简单的解决方案来解决我的问题,但这会让我非常难过,我会讨厌EF4-POCO-MVC的胜利:(。此外,这意味着EF无论使用哪种EF4对象类型,在MVC环境中都无法执行此操作:(。我认为基于快照的更改跟踪应该发现已更改的BlogPost与具有现有PK的标签存在关系。
顺便说一句:我认为同样的问题也会在一对多关系中发生(Google和我的同事这么说)。我会在家里试一试,但即使这样做可以解决问题,在我的应用程序中的六个多对多关系中也没有帮助我:(。"

请发布您的代码。这是一个常见的情况。 - John Farrell
1
我对这个问题有一个自动化的解决方案,它隐藏在下面的答案中,所以很多人可能会错过,但请看一下,它将为您节省大量的工作在此处查看帖子 - refactorthis
@brentmckendrick 我认为另一种方法更好。与其通过网络发送整个修改后的对象图,为什么不只发送增量呢?在这种情况下,您甚至不需要生成DTO类。如果您对此有任何意见,请让我们在https://dev59.com/7UnSa4cB1Zd3GeqPKxjw上进行讨论。 - HappyNomad
5个回答

148

让我们尝试这种方式:

  • 将BlogPost附加到上下文。将对象附加到上下文后,对象的状态、所有相关对象和所有关系的状态都设置为未更改。
  • 使用context.ObjectStateManager.ChangeObjectState将您的BlogPost设置为已修改
  • 遍历标签集合
  • 使用context.ObjectStateManager.ChangeRelationshipState设置当前标签与BlogPost之间的关系状态。
  • SaveChanges

编辑:

我想我的一条评论给了你错误的希望,认为EF会为你做合并。我在解决这个问题上花费了很多时间,我的结论是EF不会为你做这件事。我认为您也找到了我的MSDN上的问题。实际上,互联网上有很多这样的问题。问题在于没有清楚地说明如何处理这种情况。所以让我们来看看问题:

问题背景

EF需要跟踪实体的更改,以便持久化知道哪些记录必须更新、插入或删除。问题是ObjectContext负责跟踪更改。ObjectContext只能跟踪附加的实体的更改。在ObjectContext之外创建的实体根本没有被跟踪。

问题描述

根据上述描述,我们可以清楚地说明EF更适用于连接方案,其中实体始终附加到上下文中-这是WinForm应用程序的典型情况。Web应用程序需要断开连接的方案,在处理请求后关闭上下文,并将实体内容作为HTTP响应传递给客户端。下一个HTTP请求提供了修改后的实体内容,必须重新创建、附加到新的上下文并持久化。重新创建通常发生在上下文范围之外(具有持久性忽略的分层架构)。 解决方案 那么如何处理这种断开连接的情况呢?使用POCO类时,我们有3种处理更改跟踪的方式:
- 快照-需要相同的上下文=对于断开连接的方案无用 - 动态跟踪代理-需要相同的上下文=对于断开连接的方案无用 - 手动同步。
单个实体的手动同步是一项容易的任务。您只需要附加实体并调用AddObject进行插入,DeleteObject进行删除或在ObjectStateManager中设置状态为Modified进行更新。当您不得不处理对象图而不是单个实体时,真正的痛苦就来了。当您不得不处理独立关联(不使用外键属性的关联)和多对多关系时,这种痛苦甚至更加严重。在这种情况下,您不仅必须手动同步对象图中的每个实体,还必须手动同步对象图中的每个关系。
MSDN文档建议使用手动同步作为解决方案:附加和分离对象说:
对象以未更改状态附加到对象上下文中。如果您需要更改对象的状态或关系,因为您知道您的对象是在分离状态下修改的,请使用以下方法之一。提到的方法是ObjectStateManager的ChangeObjectState和ChangeRelationshipState =手动更改跟踪。其他MSDN文档文章中也提出了类似的建议:定义和管理关系说:如果您正在使用断开的对象,则必须手动管理同步。此外,还有一个与EF v1相关的博客帖子,批评了EF的这种行为。 解决方案原因 EF有很多“有用”的操作和设置,如Refresh, Load, ApplyCurrentValues, ApplyOriginalValues, MergeOption等。但是根据我的调查,所有这些功能都仅适用于单个实体,并且仅影响标量属性(不包括导航属性和关系)。我宁愿不使用这些方法来测试实体中嵌套的复杂类型。 其他建议的解决方案 相比提供真正合并功能,EF团队提供了一种称为Self Tracking Entities(STE)的东西,但它并没有解决问题。首先,如果整个处理过程中使用了同一实例,则STE才能起作用。在Web应用程序中,除非您将实例存储在视图状态或会话中,否则情况并非如此。因此,我对使用EF感到非常不满意,我将检查NHibernate的功能。第一个观察结果表明,NHibernate可能具有这样的功能结论

我将以单个链接结束这些假设,链接到MSDN论坛上的另一个相关问题。查看Zeeshan Hirani的回答。他是Entity Framework 4.0 Recipes的作者。如果他说不支持对象图的自动合并,我相信他。

但仍有可能我完全错了,EF中存在一些自动合并功能。

编辑2:

正如您所看到的,这在2007年已经被添加到MS Connect作为建议。微软已将其关闭,并表示将在下一个版本中完成,但实际上除STE外没有任何改进。


9
这是我在SO上读过的最好的答案之一。你清楚地阐述了许多MSDN文章、文档和博客所未能传达的内容。EF4并不本质上支持从“分离”的实体更新关系,它只提供了工具让你自己实现。谢谢! - tyriker
1
那么几个月过去了,NHibernate在这个问题上与EF4相比如何? - CallMeLaNN
1
这在NHibernate中得到了很好的支持 :-) 不需要手动合并,在我的例子中,它是一个三级深度对象图,问题有答案,每个答案都有评论,问题也有评论。无论对象图多么复杂,NHibernate都可以持久化/合并它。http://www.ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html 另一个满意的NHibernate用户:http://www.codinginstinct.com/2009/11/nhibernate-feature-saveorupdatecopy.html - Michael Buen
2
我所看过的最好的解释之一!!非常感谢。 - marvelTracker
2
EF团队计划在EF6之后解决这个问题。你可能想要为http://entityframework.codeplex.com/workitem/864投票。 - Eric J.
显示剩余13条评论

19

我有一个解决方案,可以解决Ladislav之前描述的问题。我已经为DbContext创建了一个扩展方法,它将根据提供的图形和持久性图形之间的差异自动执行添加/更新/删除操作。

目前使用Entity Framework,您需要手动更新联系人,检查每个联系人是否是新的并添加,检查是否已更新并进行编辑,检查是否已删除然后从数据库中删除它。一旦您必须在大型系统中为几个不同的聚合执行此操作,您就开始意识到必须有更好、更通用的方法。

请看一下这里的链接,看看它是否能够帮助你 http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a-graph-of-detached-entities/

你可以直接在这里查看代码 https://github.com/refactorthis/GraphDiff


我相信你能轻松解决这个问题,我对此感到很棘手。 - Shimmy Weitzhandler
1
嗨,Shimmy,抱歉最近才有时间看。我今晚会研究一下。 - refactorthis
这个库非常棒,节省了我很多时间!谢谢! - lordjeb

9
我知道这对于提问者来说已经有点晚了,但由于这是一个非常常见的问题,我还是发布了这个帖子,以便为其他人提供帮助。我一直在探索这个问题,我认为我找到了一个相当简单的解决方案,具体步骤如下:
  1. 将主对象(例如博客)的状态设置为 Modified 并保存。
  2. 查询包括我需要更新的集合在内的更新后的对象。
  3. 查询并转换 .ToList(),以包含我想要的实体集合。
  4. 将主对象的集合更新为第三步得到的列表。
  5. SaveChanges();
在下面的示例中,“dataobj”和“_categories”是我的控制器接收到的参数。“dataobj”是我的主对象,“_categories”是一个IEnumerable,其中包含用户在视图中选择的类别的 ID。
    db.Entry(dataobj).State = EntityState.Modified;
    db.SaveChanges();
    dataobj = db.ServiceTypes.Include(x => x.Categories).Single(x => x.Id == dataobj.Id);
    var it = _categories != null ? db.Categories.Where(x => _categories.Contains(x.Id)).ToList() : null;
    dataobj.Categories = it;
    db.SaveChanges();

它甚至可以用于多个关系


7
实体框架团队意识到这是一个可用性问题,并计划在 EF6 后解决它。
来自实体框架团队的消息:
这是一个我们已经意识到并正在思考的可用性问题,我们打算在 EF6 后继续努力。我创建了此工作项以跟踪此问题:http://entityframework.codeplex.com/workitem/864该工作项还包含此问题的用户声音项的链接--如果您还没有投票,请投票支持它。
如果这对您有影响,请在以下网址为其投票: http://entityframework.codeplex.com/workitem/864

EF6之后?在最乐观的情况下,那会是哪一年呢? - quetzalcoatl
@quetzalcoatl:至少这在他们的雷达上 :-) EF自EF 1以来已经走了很长的路,但仍有很长的路要走。 - Eric J.

1
所有的答案都很好地解释了问题,但是没有一个真正解决了我的问题。
我发现,如果我不使用父实体中的关系,而只是添加和删除子实体,那么一切都会很好地运行。
对不起,我用的是VB,但这就是我正在工作的项目所写的。
父实体“报告”与“报告角色”之间存在一对多的关系,并具有属性“ReportRoles”。新角色通过逗号分隔的字符串从Ajax调用中传递。
第一行将删除所有子实体,如果我使用“report.ReportRoles.Remove(f)”而不是“db.ReportRoles.Remove(f)”,我会得到错误。
report.ReportRoles.ToList.ForEach(Function(f) db.ReportRoles.Remove(f))
Dim newRoles = If(String.IsNullOrEmpty(model.RolesString), New String() {}, model.RolesString.Split(","))
newRoles.ToList.ForEach(Function(f) db.ReportRoles.Add(New ReportRole With {.ReportId = report.Id, .AspNetRoleId = f}))

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