Повторное присоединение графа объекта к EntityContext: «невозможно отслеживать несколько объектов с одним и тем же ключом» - PullRequest
2 голосов
/ 19 мая 2010

Может ли 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
}

Ответы [ 2 ]

4 голосов
/ 19 мая 2010

Проблема в том, что вы изменили значение по умолчанию MergeOption:

context1.Reports.MergeOption = MergeOption.NoTracking;

Объекты, извлеченные с помощью NoTracking, предназначены для только для чтения использования, поскольку исправлений нет; это в документации для MergeOption. Поскольку вы установили NoTracking, теперь у вас есть две совершенно разные копии {User: "JohnDoe"}; без исправления «дубликаты» ссылок не сводятся к одному экземпляру.

Теперь, когда вы пытаетесь сохранить «обе» копии {User: "JohnDoe"}, первая успешно добавляется в контекст, но вторая не может быть добавлена ​​из-за нарушения ключа.

3 голосов
/ 20 мая 2010

После прочтения документации EF снова (материал v4 - это лучше, чем 3.5) и прочтения этого поста , я понял проблему - и обойти.

При MergeOption.NoTracking EF создает граф объектов, где каждая ссылка на сущность является отдельным экземпляром сущности . Так что в моем примере обе пользовательские ссылки на 2 ReportEdits являются различными объектами - даже если все их свойства одинаковы. Они оба находятся в отдельном состоянии, и у них обоих есть EntityKeys с одинаковым значением.

Проблема в том, что при использовании метода Attach в ObjectContext контекст повторно присоединяет каждый экземпляр User, основываясь на том факте, что они являются отдельными экземплярами - он игнорирует тот факт, что они имеют один и тот же EntityKey .

Такое поведение имеет смысл, я полагаю. Если объекты находятся в отсоединенном состоянии, EF не знает, была ли изменена одна из двух ссылок и т. Д. Поэтому вместо того, чтобы предполагать, что они оба неизменны, и рассматривать их как равные, мы получаем исключение InvalidOperationException.

Но что, если, как и в моем случае, вы знаете, что обе пользовательские ссылки в отсоединенном состоянии на самом деле одинаковы и хотите, чтобы при повторном их присоединении они рассматривались как равные? Оказывается, решение достаточно простое: Если на графе ссылаются на сущность несколько раз, каждая из этих ссылок должна указывать на один экземпляр объекта .

Используя 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);
                }
            }
        }
    }
}
...