将对象图重新附加到 EntityContext:“无法跟踪具有相同键的多个对象”
EF真的这么差吗?也许...
假设我有一个完全加载的、断开连接的对象图,如下所示:
myReport =
{Report}
{ReportEdit {User: "JohnDoe"}}
{ReportEdit {User: "JohnDoe"}}
基本上是一个包含由同一用户完成的 2 次编辑的报告。
然后我这样做:
EntityContext.Attach(myReport);
InvalidOperationException:ObjectStateManager 中已存在具有相同键的对象。 ObjectStateManager 无法跟踪具有相同键的多个对象。
为什么?因为 EF 正在尝试附加 {User: "JohnDoe"}
实体两次。
这将起作用:
myReport =
{Report}
{ReportEdit {User: "JohnDoe"}}
EntityContext.Attach(myReport);
这里没有问题,因为 {User: "JohnDoe"}
实体仅在对象图中出现一次。
更重要的是,由于您无法控制 EF 如何附加实体,因此无法阻止它附加整个对象图。因此,如果您想重新附加一个包含多个对同一实体的引用的复杂实体……那么,祝您好运。
至少在我看来是这样。有什么意见吗?
更新:添加了示例代码:
// Load the report
Report theReport;
using (var context1 = new TestEntities())
{
context1.Reports.MergeOption = MergeOption.NoTracking;
theReport = (from r in context1.Reports.Include("ReportEdits.User")
where r.Id == reportId
select r).First();
}
// theReport looks like this:
// {Report[Id=1]}
// {ReportEdit[Id=1] {User[Id=1,Name="John Doe"]}
// {ReportEdit[Id=2] {User[Id=1,Name="John Doe"]}
// Try to re-attach the report object graph
using (var context2 = new TestEntities())
{
context2.Attach(theReport); // InvalidOperationException
}
Can EF really be this bad? Maybe...
Let's say I have a fully loaded, disconnected object graph that looks like this:
myReport =
{Report}
{ReportEdit {User: "JohnDoe"}}
{ReportEdit {User: "JohnDoe"}}
Basically a report with 2 edits that were done by the same user.
And then I do this:
EntityContext.Attach(myReport);
InvalidOperationException: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
Why? Because the EF is trying to attach the {User: "JohnDoe"}
entity TWICE.
This will work:
myReport =
{Report}
{ReportEdit {User: "JohnDoe"}}
EntityContext.Attach(myReport);
No problems here because the {User: "JohnDoe"}
entity only appears in the object graph once.
What's more, since you can't control how the EF attaches an entity, there is no way to stop it from attaching the entire object graph. So really if you want to reattach a complex entity that contains more than one reference to the same entity... well, good luck.
At least that's how it looks to me. Any comments?
UPDATE: Added sample code:
// Load the report
Report theReport;
using (var context1 = new TestEntities())
{
context1.Reports.MergeOption = MergeOption.NoTracking;
theReport = (from r in context1.Reports.Include("ReportEdits.User")
where r.Id == reportId
select r).First();
}
// theReport looks like this:
// {Report[Id=1]}
// {ReportEdit[Id=1] {User[Id=1,Name="John Doe"]}
// {ReportEdit[Id=2] {User[Id=1,Name="John Doe"]}
// Try to re-attach the report object graph
using (var context2 = new TestEntities())
{
context2.Attach(theReport); // InvalidOperationException
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
问题是您修改了默认的
MergeOption
:使用
NoTracking
检索的实体仅供只读使用,因为没有修复;这位于MergeOption
的文档中。由于您设置了NoTracking
,您现在拥有{User: "JohnDoe"}
; 的两个完全独立的副本;如果不进行修复,“重复”引用不会归结为单个实例。现在,当您尝试保存
{User: "JohnDoe"}
的“两个”副本时,第一个副本成功添加到上下文中,但由于密钥冲突而无法添加第二个副本。The problem is that you modified the default
MergeOption
:Entities retrieved with
NoTracking
are intended for read-only use because there is no fixup; this is in the documentation forMergeOption
. Because you setNoTracking
, you now have two entirely separate copies of{User: "JohnDoe"}
; without fixup the "duplicate" references don't get boiled down to a single instance.Now when you try to save "both" copies of
{User: "JohnDoe"}
, the first succeeds in being added to the context, but the second can't be added because of the key violation.再次阅读 EF 文档(v4 的内容 - 它比 3.5 的内容更好)并阅读 这篇文章,我意识到了这个问题 - 以及解决方法。
通过
MergeOption.NoTracking
,EF 创建一个对象图,其中每个实体引用都是该实体的一个不同实例。因此,在我的示例中,2 个 ReportEdit 上的两个 User 引用都是不同的对象 - 即使它们的所有属性都相同。它们都处于 Detached 状态,并且它们的 EntityKey 值相同。问题是,当在 ObjectContext 上使用 Attach 方法时,上下文会根据每个 User 实例是单独实例的事实重新附加它们 - 它忽略它们具有相同 EntityKey 的事实。
我认为这种行为是有道理的。如果实体处于分离状态,则 EF 不知道两个引用之一是否已被修改,等等。因此,我们不会假设它们都未更改并将它们视为相等,而是会收到 InvalidOperationException。
但是,如果像我的情况一样,您知道分离状态下的两个 User 引用实际上是相同的,并且希望在重新连接时将它们视为相等,该怎么办?事实证明,解决方案非常简单:如果一个实体在图中被多次引用,则每个引用都需要指向该对象的单个实例。
使用
IEntityWithRelationships
,我们可以遍历分离的对象图并更新引用并合并对同一实体实例的重复引用。然后,ObjectContext 会将任何重复的实体引用视为同一实体并重新附加它,而不会出现任何错误。大致基于我上面引用的博客文章,我创建了一个类来合并对重复实体的引用,以便它们共享相同的对象引用。请记住,如果在分离状态下修改了任何重复引用,您最终将得到不可预测的结果:图中找到的第一个实体始终优先。但在特定情况下,它似乎能起到作用。
After reading the EF documentation again (the v4 stuff - it's better than the 3.5 stuff) and reading this post, I realized the issue - and a work around.
With
MergeOption.NoTracking
, the EF creates an object graph where each entity reference is a distinct instance of the entity. So in my example, both User references on the 2 ReportEdits are distinct objects - even though all their properties are the same. They are both in the Detached state, and they both have EntityKeys with the same value.The trouble is, when using the Attach method on the ObjectContext, the context reattaches each User instance based on the fact that they are separate instances - it ignores the fact that they have the same EntityKey.
This behavior makes sense, I suppose. If the entities are in the detached state, the EF doesn't know if one of the two references has been modified, etc. So instead of assuming they are both unchanged and treating them as equal, we get an InvalidOperationException.
But what if, like in my case, you know that both User references in the detached state are in fact the same and want them to be treated as equal when they are reattached? Turns out the solution is simple enough: If an entity is referenced multiple times in the graph, each one of those references needs to point to a single instance of the object.
Using the
IEntityWithRelationships
, we can traverse the detached object graph and update the references and consolidate duplicate references to the same entity instance. ObjectContext will then treat any duplicate entity references as the same entity and reattach it without any error.Based loosely on the blog post I referenced above, I've created a class to consolidate references to duplicate entities so that they share the same object reference. Keep in mind that if any of the duplicate references have been modfied while in the detached state, you'll end up with unpredicatable results: the first entity found in the graph always takes precedence. In specific scenarios though, it seems to do the trick.