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

Posted

技术标签:

【中文标题】保存 EF4 POCO 对象的更改时更新关系【英文标题】:Update relationships when saving changes of EF4 POCO objects 【发布时间】:2011-04-07 19:07:50 【问题描述】:

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 应用程序中的视图)。稍后我取回更新后的 BlogPost,其中包含更改的属性和更改的关系。例如,它有标签“A”、“B”和“C”,新标签是“C”和“D”。在我的特定示例中,没有新的标签,并且标签的属性永远不会改变,所以唯一应该保存的是改变的关系。现在我需要将它保存在另一个 ObjectContext 中。 (更新:现在我尝试在同一个上下文实例中进行操作,但也失败了。)

问题:我不能让它正确地保存关系。我尝试了所有找到的方法:

Controller.UpdateModel 和 Controller.TryUpdateModel 不起作用。 从上下文中获取旧的 BlogPost 然后修改集合不起作用。 (与下一点不同的方法) This 可能会起作用,但我希望这只是一种解决方法,而不是解决方案:(。 尝试了以各种可能的组合为 BlogPost 和/或标签添加/添加/更改对象状态功能。失败。 This 看起来像我需要的,但它不起作用(我试图修复它,但无法解决我的问题)。 尝试过ChangeState/Add/Attach/...上下文的关系对象。失败。

“不起作用”在大多数情况下意味着我在给定的“解决方案”上工作,直到它没有产生错误并至少保存了 BlogPost 的属性。关系会发生什么变化:通常标签会使用新的 PK 再次添加到标签表中,并且保存的 BlogPost 引用这些而不是原始的。当然,返回的标签有 PK,在保存/更新方法之前,我检查了 PK,它们与数据库中的相同,所以 EF 可能认为它们是新对象,而这些 PK 是临时对象。

我知道并且可能无法找到自动化简单解决方案的问题:当 POCO 对象的集合发生更改时,上述虚拟集合属性应该会发生这种情况,因为这样 FixupCollection 技巧将更新反向引用多对多关系的另一端。然而,当视图“返回”一个更新的 BlogPost 对象时,这并没有发生。这意味着我的问题可能没有简单的解决方案,但这会让我非常难过,我会讨厌 EF4-POCO-MVC 的胜利:(。这也意味着 EF 不能在 MVC 环境中做到这一点。使用 EF4 对象类型:(。我认为基于快照的更改跟踪应该发现更改后的 BlogPost 与具有现有 PK 的标签有关系。

顺便说一句:我认为一对多关系也会出现同样的问题(谷歌和我的同事都这么说)。我会在家里试一试,但即使这对我的应用程序中的六个多对多关系没有帮助:(。

【问题讨论】:

请发布您的代码。这是一种常见的情况。 我有这个问题的自动解决方案,它隐藏在下面的答案中,所以很多人会错过它,但请看一下,因为它会为你节省一份工作see post here @brentmckendrick 我认为另一种方法更好。与其通过网络发送整个修改后的对象图,不如直接发送增量?在这种情况下,您甚至不需要生成的 DTO 类。如果您对此有任何意见,请联系***.com/questions/1344066/calculate-object-delta 讨论。 【参考方案1】:

让我们这样试试吧:

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

编辑:

我猜我的一个 cmets 给了你错误的希望 EF 会为你做合并。我在这个问题上玩了很多,我的结论是 EF 不会为你做这件事。我想你也在MSDN 上找到了我的问题。事实上,互联网上有很多这样的问题。问题是没有明确说明如何处理这种情况。那么让我们来看看这个问题:

问题背景

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

问题描述

基于上面的描述,我们可以清楚地说明 EF 更适合实体始终附加到上下文的连接场景——典型的 WinForm 应用程序。 Web 应用程序需要断开连接的场景,其中上下文在请求处理后关闭,实体内容作为 HTTP 响应传递给客户端。下一个 HTTP 请求提供实体的修改内容,这些内容必须重新创建、附加到新上下文并持久化。娱乐通常发生在上下文范围之外(具有持久性无知的分层架构)。

解决方案

那么如何处理这种断开连接的情况呢?使用 POCO 类时,我们有 3 种方法来处理更改跟踪:

快照 - 需要相同的上下文 = 对于断开连接的场景无用 动态跟踪代理 - 需要相同的上下文 = 对于断开连接的场景无用 手动同步。

单个实体上的手动同步很容易。您只需要附加实体并调用 AddObject 进行插入,DeleteObject 删除或将 ObjectStateManager 中的状态设置为 Modified 进行更新。当您必须处理对象图而不是单个实体时,真正的痛苦就来了。当您必须处理独立关联(那些不使用外键属性的关联)和多对多关系时,这种痛苦会更加严重。在这种情况下,您必须手动同步对象图中的每个实体以及对象图中的每个关系。

MSDN 文档提出了手动同步作为解决方案:Attaching and Detaching objects 说:

对象附加到对象 处于未更改状态的上下文。如果你 需要改变一个对象的状态 或者因为你知道的关系 你的对象被修改了 分离状态,使用其中之一 以下方法。

提到的方法是 ObjectStateManager 的 ChangeObjectState 和 ChangeRelationshipState = 手动更改跟踪。类似的提议在其他 MSDN 文档文章中:Defining and Managing Relationships 说:

如果您正在使用断开连接 您必须手动管理的对象 同步。

此外,与 EF v1 相关的 blog post 正是批评 EF 的这种行为。

解决的原因

EF 有许多“有用”的操作和设置,例如 Refresh、Load、ApplyCurrentValues、ApplyOriginalValues、MergeOption 等。但根据我的调查,所有这些功能仅适用于单个实体,并且仅影响标量preperties(= 不是导航属性和关系)。我宁愿不使用嵌套在实体中的复杂类型来测试这种方法。

其他建议的解决方案

EF 团队提供了名为 Self Tracking Entities (STE) 的东西,而不是真正的合并功能,但它并不能解决问题。首先,只有在整个处理过程中使用相同的实例时,STE 才有效。在 Web 应用程序中,除非您将实例存储在视图状态或会话中,否则情况并非如此。因此,我对使用 EF 非常不满意,我将检查 NHibernate 的功能。第一个观察表明 NHibernate 可能有这样的functionality。

结论

我将在 MSDN 论坛上通过指向另一个 related question 的单个链接来结束这个假设。检查 Zeeshan Hirani 的答案。他是Entity Framework 4.0 Recipes 的作者。如果他说不支持对象图的自动合并,我相信他。

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

编辑 2:

如您所见,这已在 2007 年作为建议添加到 MS Connect。MS 已将其关闭,作为下一版本中要做的事情,但实际上除了 STE 之外没有做任何事情来改善这个差距。

【讨论】:

这是我在 SO 上读到的最佳答案之一。您已经清楚地说明了这么多关于该主题的 MSDN 文章、文档和博客文章未能传达的内容。 EF4 本身并不支持从“分离”实体更新关系。它只为您提供自己实现它的工具。谢谢! 那么几个月过去了,与 EF4 相比,与此问题相关的 NHibernate 怎么样? 这在 NHibernate 中得到了很好的支持 :-) 无需手动合并,在我的示例中它是 3 级深度对象图,问题有答案,每个答案都有 cmets,并且问题也有 cmets。 NHibernate 可以持久化/合并您的对象图,无论它多么复杂ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html 另一个满意的 NHibernate 用户:codinginstinct.com/2009/11/… 我读过的最好的解释之一!非常感谢 EF 团队计划解决这个 EF6 后的问题。你可能想投票给entityframework.codeplex.com/workitem/864【参考方案2】:

我对 Ladislav 上面描述的问题有一个解决方案。我为 DbContext 创建了一个扩展方法,它将根据提供的图和持久图的差异自动执行添加/更新/删除。

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

请看看有没有帮助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

【讨论】:

我相信你可以轻松解决this 的问题,我玩得很不爽。 嗨 Shimmy,抱歉终于有时间看看了。今晚我会调查一下。 这个库很棒,为我节省了很多时间!谢谢!【参考方案3】:

我知道 OP 已经晚了,但由于这是一个非常常见的问题,所以我发布了这个问题,以防它为其他人服务。 我一直在玩弄这个问题,我想我有一个相当简单的解决方案, 我要做的是:

    通过将其状态设置为已修改来保存主要对象(例如博客)。 在数据库中查询更新的对象,包括我需要更新的集合。 查询并转换 .ToList() 我希望我的集合包含的实体。 将主对象的集合更新为我从第 3 步获得的列表。 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();

它甚至适用于多种关系

【讨论】:

【参考方案4】:

Entity Framework 团队意识到这是一个可用性问题,并计划在 EF6 之后解决它。

来自实体框架团队:

这是一个我们已经意识到的可用性问题,也是我们一直在考虑并计划在 EF6 后做更多工作的问题。我创建了这个工作项来跟踪问题:http://entityframework.codeplex.com/workitem/864 该工作项还包含一个指向用户语音项的链接——如果你还没有这样做,我鼓励你投票。

如果这对您有影响,请投票支持该功能

http://entityframework.codeplex.com/workitem/864

【讨论】:

EF6 后?乐观的情况下会是哪一年? @quetzalcoatl:至少在他们的雷达上 :-) 自 EF 1 以来,EF 已经取得了长足的进步,但仍有一段路要走。【参考方案5】:

所有答案都很好地解释了这个问题,但没有一个能真正为我解决问题。

我发现,如果我不使用父实体中的关系,而只是添加和删除子实体,一切都会正常工作。

对于 VB 很抱歉,但这就是我正在从事的项目所写的内容。

父实体“Report”与“ReportRole”具有一对多关系,并具有“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))

【讨论】:

以上是关于保存 EF4 POCO 对象的更改时更新关系的主要内容,如果未能解决你的问题,请参考以下文章

N-Tier - 插入与更新的责任位置

EF4 与 SQL Compact 4,rowversion 在保存时不更新

EF4 POCO:快照与 WCF 上的自我跟踪

使用 EF4 的 POCO 模板时“找不到元数据信息”?

`NSFetchedResultsController` 从子上下文更新关系时不刷新

EF4 CTP5 POCO 中的软删除、导航属性