将对象图重新附加到 EntityContext:“无法跟踪具有相同键的多个对象”

发布于 2024-09-02 03:45:28 字数 1414 浏览 5 评论 0原文

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

寻梦旅人 2024-09-09 03:45:28

问题是您修改了默认的 MergeOption

context1.Reports.MergeOption = MergeOption.NoTracking;

使用 NoTracking 检索的实体仅供只读使用,因为没有修复;这位于 MergeOption 的文档中。由于您设置了 NoTracking,您现在拥有 {User: "JohnDoe"}; 的两个完全独立的副本;如果不进行修复,“重复”引用不会归结为单个实例。

现在,当您尝试保存 {User: "JohnDoe"} 的“两个”副本时,第一个副本成功添加到上下文中,但由于密钥冲突而无法添加第二个副本。

The problem is that you modified the default MergeOption:

context1.Reports.MergeOption = MergeOption.NoTracking;

Entities retrieved with NoTracking are intended for read-only use because there is no fixup; this is in the documentation for MergeOption. Because you set NoTracking, 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.

野味少女 2024-09-09 03:45:28

再次阅读 EF 文档(v4 的内容 - 它比 3.5 的内容更好)并阅读 这篇文章,我意识到了这个问题 - 以及解决方法。

通过 MergeOption.NoTracking,EF 创建一个对象图,其中每个实体引用都是该实体的一个不同实例。因此,在我的示例中,2 个 ReportEdit 上的两个 User 引用都是不同的对象 - 即使它们的所有属性都相同。它们都处于 Detached 状态,并且它们的 EntityKey 值相同。

问题是,当在 ObjectContext 上使用 Attach 方法时,上下文会根据每个 User 实例是单独实例的事实重新附加它们 - 它忽略它们具有相同 EntityKey 的事实。

我认为这种行为是有道理的。如果实体处于分离状态,则 EF 不知道两个引用之一是否已被修改,等等。因此,我们不会假设它们都未更改并将它们视为相等,而是会收到 InvalidOperationException。

但是,如果像我的情况一样,您知道分离状态下的两个 User 引用实际上是相同的,并且希望在重新连接时将它们视为相等,该怎么办?事实证明,解决方案非常简单:如果一个实体在图中被多次引用,则每个引用都需要指向该对象的单个实例

使用IEntityWithRelationships,我们可以遍历分离的对象图并更新引用并合并对同一实体实例的重复引用。然后,ObjectContext 会将任何重复的实体引用视为同一实体并重新附加它,而不会出现任何错误。

大致基于我上面引用的博客文章,我创建了一个类来合并对重复实体的引用,以便它们共享相同的对象引用。请记住,如果在分离状态下修改了任何重复引用,您最终将得到不可预测的结果:图中找到的第一个实体始终优先。但在特定情况下,它似乎能起到作用。


public class EntityReferenceManager
{
    /// 
    /// A mapping of the first entity found with a given key.
    /// 
    private Dictionary _entityMap;

    /// 
    /// Entities that have been searched already, to limit recursion.
    /// 
    private List _processedEntities;

    /// 
    /// Recursively searches through the relationships on an entity
    /// and looks for duplicate entities based on their EntityKey.
    /// 
    /// If a duplicate entity is found, it is replaced by the first
    /// existing entity of the same key (regardless of where it is found 
    /// in the object graph).
    /// 
    /// 
    public void ConsolidateDuplicateRefences(IEntityWithRelationships ewr)
    {
        _entityMap = new Dictionary();
        _processedEntities = new List();

        ConsolidateDuplicateReferences(ewr, 0);

        _entityMap = null;
        _processedEntities = null;
    }

    private void ConsolidateDuplicateReferences(IEntityWithRelationships ewr, int level)
    {
        // Prevent unlimited recursion
        if (_processedEntities.Contains(ewr))
        {
            return;
        }
        _processedEntities.Add(ewr);

        foreach (var end in ewr.RelationshipManager.GetAllRelatedEnds())
        {
            if (end is IEnumerable)
            {
                // The end is a collection of entities
                var endEnum = (IEnumerable)end;
                foreach (var endValue in endEnum)
                {
                    if (endValue is IEntityWithKey)
                    {
                        var entity = (IEntityWithKey)endValue;
                        // Check if an object with the same key exists elsewhere in the graph
                        if (_entityMap.ContainsKey(entity.EntityKey))
                        {
                            // Check if the object reference differs from the existing entity
                            if (_entityMap[entity.EntityKey] != entity)
                            {
                                // Two objects with the same key in an EntityCollection - I don't think it's possible to fix this... 
                                // But can it actually occur in the first place?
                                throw new NotSupportedException("Cannot handle duplicate entities in a collection");
                            }
                        }
                        else
                        {
                            // First entity with this key in the graph
                            _entityMap.Add(entity.EntityKey, entity);
                        }
                    }
                    if (endValue is IEntityWithRelationships)
                    {
                        // Recursively process relationships on this entity
                        ConsolidateDuplicateReferences((IEntityWithRelationships)endValue, level + 1);
                    }
                }
            }
            else if (end is EntityReference) 
            {
                // The end is a reference to a single entity
                var endRef = (EntityReference)end;
                var pValue = endRef.GetType().GetProperty("Value");
                var endValue = pValue.GetValue(endRef, null);
                if (endValue is IEntityWithKey)
                {
                    var entity = (IEntityWithKey)endValue;
                    // Check if an object with the same key exists elsewhere in the graph
                    if (_entityMap.ContainsKey(entity.EntityKey))
                    {
                        // Check if the object reference differs from the existing entity
                        if (_entityMap[entity.EntityKey] != entity)
                        {
                            // Update the reference to the existing entity object
                            pValue.SetValue(endRef, _entityMap[endRef.EntityKey], null);
                        }
                    }
                    else
                    {
                        // First entity with this key in the graph
                        _entityMap.Add(entity.EntityKey, entity);
                    }
                }
                if (endValue is IEntityWithRelationships)
                {
                    // Recursively process relationships on this entity
                    ConsolidateDuplicateReferences((IEntityWithRelationships)endValue, level + 1);
                }
            }
        }
    }
}

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.


public class EntityReferenceManager
{
    /// 
    /// A mapping of the first entity found with a given key.
    /// 
    private Dictionary _entityMap;

    /// 
    /// Entities that have been searched already, to limit recursion.
    /// 
    private List _processedEntities;

    /// 
    /// Recursively searches through the relationships on an entity
    /// and looks for duplicate entities based on their EntityKey.
    /// 
    /// If a duplicate entity is found, it is replaced by the first
    /// existing entity of the same key (regardless of where it is found 
    /// in the object graph).
    /// 
    /// 
    public void ConsolidateDuplicateRefences(IEntityWithRelationships ewr)
    {
        _entityMap = new Dictionary();
        _processedEntities = new List();

        ConsolidateDuplicateReferences(ewr, 0);

        _entityMap = null;
        _processedEntities = null;
    }

    private void ConsolidateDuplicateReferences(IEntityWithRelationships ewr, int level)
    {
        // Prevent unlimited recursion
        if (_processedEntities.Contains(ewr))
        {
            return;
        }
        _processedEntities.Add(ewr);

        foreach (var end in ewr.RelationshipManager.GetAllRelatedEnds())
        {
            if (end is IEnumerable)
            {
                // The end is a collection of entities
                var endEnum = (IEnumerable)end;
                foreach (var endValue in endEnum)
                {
                    if (endValue is IEntityWithKey)
                    {
                        var entity = (IEntityWithKey)endValue;
                        // Check if an object with the same key exists elsewhere in the graph
                        if (_entityMap.ContainsKey(entity.EntityKey))
                        {
                            // Check if the object reference differs from the existing entity
                            if (_entityMap[entity.EntityKey] != entity)
                            {
                                // Two objects with the same key in an EntityCollection - I don't think it's possible to fix this... 
                                // But can it actually occur in the first place?
                                throw new NotSupportedException("Cannot handle duplicate entities in a collection");
                            }
                        }
                        else
                        {
                            // First entity with this key in the graph
                            _entityMap.Add(entity.EntityKey, entity);
                        }
                    }
                    if (endValue is IEntityWithRelationships)
                    {
                        // Recursively process relationships on this entity
                        ConsolidateDuplicateReferences((IEntityWithRelationships)endValue, level + 1);
                    }
                }
            }
            else if (end is EntityReference) 
            {
                // The end is a reference to a single entity
                var endRef = (EntityReference)end;
                var pValue = endRef.GetType().GetProperty("Value");
                var endValue = pValue.GetValue(endRef, null);
                if (endValue is IEntityWithKey)
                {
                    var entity = (IEntityWithKey)endValue;
                    // Check if an object with the same key exists elsewhere in the graph
                    if (_entityMap.ContainsKey(entity.EntityKey))
                    {
                        // Check if the object reference differs from the existing entity
                        if (_entityMap[entity.EntityKey] != entity)
                        {
                            // Update the reference to the existing entity object
                            pValue.SetValue(endRef, _entityMap[endRef.EntityKey], null);
                        }
                    }
                    else
                    {
                        // First entity with this key in the graph
                        _entityMap.Add(entity.EntityKey, entity);
                    }
                }
                if (endValue is IEntityWithRelationships)
                {
                    // Recursively process relationships on this entity
                    ConsolidateDuplicateReferences((IEntityWithRelationships)endValue, level + 1);
                }
            }
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文