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

实体框架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 中。(更新: 现在我尝试在同一个上下文实例中执行,但也失败了。)

问题是: 我不能正确地挽救我们的关系,我尝试了所有我发现的方法:

  • 控制器。 UpdateModel 和 Controller。 TryUpdateModel 不工作。
  • 从上下文获取旧的 BlogPost,然后修改集合是不起作用的(使用下一点的不同方法)
  • 这个 可能会奏效,但我希望这只是一个变通方案,而不是解决方案:。
  • 尝试在每个可能的组合中为 BlogPost 和/或标签添加/添加/更改 ObjectState 函数。失败。
  • 这个 看起来像我需要的,但是它不工作(我试图修复它,但是对于我的问题不能)。
  • 尝试更改状态/添加/附加/... 上下文的关系对象。失败。

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

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

顺便说一句: 我认为同样的问题发生在一对多关系(谷歌和我的同事这样说)。我会在家里尝试一下,但即使这样也不能帮助我在我的应用程序的六个多对多的关系: (。

37817 次浏览

Let's try it this way:

  • Attach BlogPost to context. After attaching object to context the state of the object, all related objects and all relations is set to Unchanged.
  • Use context.ObjectStateManager.ChangeObjectState to set your BlogPost to Modified
  • Iterate through Tag collection
  • Use context.ObjectStateManager.ChangeRelationshipState to set state for relation between current Tag and BlogPost.
  • SaveChanges

Edit:

I guess one of my comments gave you false hope that EF will do the merge for you. I played a lot with this problem and my conclusion says EF will not do this for you. I think you have also found my question on MSDN. In reality there is plenty of such questions on the Internet. The problem is that it is not clearly stated how to deal with this scenario. So lets have a look on the problem:

Problem background

EF needs to track changes on entities so that persistance knows which records have to be updated, inserted or deleted. The problem is that it is ObjectContext responsibility to track changes. ObjectContext is able to track changes only for attached entities. Entities which are created outside the ObjectContext are not tracked at all.

Problem description

Based on above description we can clearly state that EF is more suitable for connected scenarios where entity is always attached to context - typical for WinForm application. Web applications requires disconnected scenario where context is closed after request processing and entity content is passed as HTTP response to the client. Next HTTP request provides modified content of the entity which has to be recreated, attached to new context and persisted. Recreation usually happends outside of the context scope (layered architecture with persistance ignorace).

Solution

So how to deal with such disconnected scenario? When using POCO classes we have 3 ways to deal with change tracking:

  • Snapshot - requires same context = useless for disconnected scenario
  • Dynamic tracking proxies - requires same context = useless for disconnected scenario
  • Manual synchronization.

Manual synchronization on single entity is easy task. You just need to attach entity and call AddObject for inserting, DeleteObject for deleting or set state in ObjectStateManager to Modified for updating. The real pain comes when you have to deal with object graph instead of single entity. This pain is even worse when you have to deal with independent associations (those that don't use Foreign Key property) and many to many relations. In that case you have to manually synchronize each entity in object graph but also each relation in object graph.

Manual synchronization is proposed as solution by MSDN documentation: Attaching and Detaching objects says:

Objects are attached to the object context in an Unchanged state. If you need to change the state of an object or the relationship because you know that your object was modified in detached state, use one of the following methods.

Mentioned methods are ChangeObjectState and ChangeRelationshipState of ObjectStateManager = manual change tracking. Similar proposal is in other MSDN documentation article: Defining and Managing Relationships says:

If you are working with disconnected objects you must manually manage the synchronization.

Moreover there is blog post related to EF v1 which criticise exactly this behavior of EF.

Reason for solution

EF has many "helpful" operations and settings like Refresh, Load, ApplyCurrentValues, ApplyOriginalValues, MergeOption etc. But by my investigation all these features work only for single entity and affects only scalar preperties (= not navigation properties and relations). I rather not test this methods with complex types nested in entity.

Other proposed solution

Instead of real Merge functionality EF team provides something called Self Tracking Entities (STE) which don't solve the problem. First of all STE works only if same instance is used for whole processing. In web application it is not the case unless you store instance in view state or session. Due to that I'm very unhappy from using EF and I'm going to check features of NHibernate. First observation says that NHibernate perhaps has such functionality.

Conclusion

I will end up this assumptions with single link to another related question on MSDN forum. Check Zeeshan Hirani's answer. He is author of Entity Framework 4.0 Recipes. If he says that automatic merge of object graphs is not supported, I believe him.

But still there is possibility that I'm completely wrong and some automatic merge functionality exists in EF.

Edit 2:

As you can see this was already added to MS Connect as suggestion in 2007. MS has closed it as something to be done in next version but actually nothing had been done to improve this gap except STE.

I know it's late for the OP but since this is a very common issue I posted this in case it serves someone else. I've been toying around with this issue and I think I got a fairly simple solution, what I do is:

  1. Save main object (Blogs for example) by setting its state to Modified.
  2. Query the database for the updated object including the collections I need to update.
  3. Query and convert .ToList() the entities I want my collection to include.
  4. Update the main object's collection(s) to the List I got from step 3.
  5. SaveChanges();

In the following example "dataobj" and "_categories" are the parameters received by my controller "dataobj" is my main object, and "_categories" is an IEnumerable containing the IDs of the categories the user selected in the view.

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

It even works for multiple relations

I have a solution to the problem that was described above by Ladislav. I have created an extension method for the DbContext which will automatically perform the add/update/delete's based on a diff of the provided graph and persisted graph.

At present using the Entity Framework you will need to perform the updates of the contacts manually, check if each contact is new and add, check if updated and edit, check if removed then delete it from the database. Once you have to do this for a few different aggregates in a large system you start to realize there must be a better, more generic way.

Please take a look and see if it can help http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a-graph-of-detached-entities/

You can go straight to the code here https://github.com/refactorthis/GraphDiff

The Entity Framework team is aware that this is a usability issue and plans to address it post-EF6.

From the Entity Framework team:

This is a usability issue that we are aware of and is something we have been thinking about and plan to do more work on post-EF6. I have created this work item to track the issue: http://entityframework.codeplex.com/workitem/864 The work item also contains a link to the user voice item for this--I encourage you to vote for it if you have not done so already.

If this impacts you, vote for the feature at

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

All of the answers were great to explain the problem, but none of them really solved the problem for me.

I found that if I didn't use the relationship in the parent entity but just added and removed the child entities everything worked just fine.

Sorry for the VB but that is what the project I am working in is written in.

The parent entity "Report" has a one to many relationship to "ReportRole" and has the property "ReportRoles". The new roles are passed in by a comma separated string from an Ajax call.

The first line will remove all the child entities, and if I used "report.ReportRoles.Remove(f)" instead of the "db.ReportRoles.Remove(f)" I would get the error.

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