Сохранение отдельных объектов с помощью кода Entity Framework - PullRequest
5 голосов
/ 02 марта 2012

Я использую Entity Framework 4.3.1 в проекте, сначала использую код и API DbContext. Мое приложение представляет собой n-уровневое приложение, в котором от клиента могут входить отключенные объекты. Я использую SQL Server 2008 R2, но скоро перейду на SQL Azure. Я столкнулся с проблемой, которую просто не могу решить.

Представьте, у меня есть несколько классов:

class A {
    // Random stuff here
}
class B {
    // Random stuff here
    public A MyA { get; set; }
}
class C {
    // Random stuff here
    public A MyA { get; set; }
}

По умолчанию EF работает с графами объектов. Например, если у меня есть экземпляр B, который инкапсулирует экземпляр A, и я вызываю myDbSet.Add(myB);, он также помечает экземпляр A как добавленный (при условии, что он еще не отслеживается).

В моем приложении есть сценарий, в котором мне нужно четко указать, какие объекты сохраняются в базе данных, а не отслеживать все графы объектов. Порядок действий следующий:

A myA = new A(); // Represents something already in DB that doesn't need to be udpated.
C myC = new C() { // Represents something already in DB that DOES need to be updated.
    A = myA;
}
B myB0 = new B() { // Not yet in DB.
    A = myA;
}
B myB1 = new B() { // Not yet in DB.
    A = myA;
}

myDbSetC.Attach(myC);
context.Entry(myC).State = Modified;

myDbSetB.Add(myB0); // Tries to track myA with a state of Added
myDbSetB.Add(myB1);

context.SaveChanges();

В этот момент я получаю сообщение об ошибке: AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges. Я полагаю, что это происходит потому, что вызов add на myB0 помечает экземпляр A как добавленный, что конфликтует с экземпляром A, который уже отслеживается.

В идеале я мог бы сделать что-то вроде вызова myDbSet.AddOnly(myB), но, очевидно, у нас нет такой возможности.

Я пробовал несколько обходных путей:

Попытка № 1: Сначала я попытался создать вспомогательный метод для предотвращения добавления myA во второй раз.

private void MarkGraphAsUnchanged<TEntity>(TEntity entity) where TEntity : class {
        DbEntityEntry entryForThis = this.context.Entry<TEntity>(entity);
        IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct();

        foreach (DbEntityEntry entry in entriesItWantsToChange) {
            if (!entryForThis.Equals(entry)) {
                entry.State = System.Data.EntityState.Unchanged;
            }
        }
    }

...

myDbSetB.Add(myB0);
MarkGraphAsUnchanged(myB0);

Хотя это решает проблему попытки добавить myA, оно по-прежнему вызывает ключевые нарушения в ObjectStateManager.

Попытка № 2: Я попытался сделать то же, что и выше, но установив состояние «Отсоединено» вместо «Без изменений». Это работает для сохранения, но настаивает на установке myB0.A = null, что имеет другие негативные последствия в моем коде.

Попытка № 3: Я использовал TransactionScope вокруг всего моего DbContext. Однако даже при вызове SaveChanges() между каждым Attach() и Add() средство отслеживания изменений не сбрасывает отслеживаемые записи, поэтому у меня возникает та же проблема, что и при попытке № 1.

Попытка № 4: Я продолжил работу с TransactionScope, за исключением того, что использовал шаблон репозитория / DAO и внутренне создал новый DbContext и вызывал SaveChanges() для каждой отдельной операции, которую я выполняю. В этом случае я получил сообщение об ошибке «Оператор Store update, insert или delete затронул неожиданное количество строк». При использовании SQL Profiler я обнаружил, что при вызове SaveChanges() в операции second , которую я сделал (первый Add()), он фактически отправляет UPDATE SQL в базу данных из первая операция во второй раз - но строки не меняются. Для меня это похоже на ошибку в Entity Framework.

Попытка № 5: Вместо использования TransactionScope я решил использовать только DbTransaction. Я все еще создаю несколько контекстов, но передаю предварительно созданный EntityConnection каждому новому контексту по мере его создания (путем кэширования и ручного открытия EntityConnection, созданного первым контекстом). Однако, когда я делаю это, во втором контексте запускается определенный мной инициализатор, даже если он уже запустился при первом запуске приложения. В среде разработчиков у меня есть это заполнение некоторых тестовых данных, и на самом деле истекает время ожидания блокировки базы данных для таблицы, которую я впервые изменил Attach() (но все еще заблокирован из-за открытой транзакции).

Помощь !! Я перепробовал все, что мог придумать, и если не считать полного рефакторинга моего приложения, чтобы не использовать свойства навигации или не использовать вручную созданные DAO для выполнения операторов INSERT, UPDATE и DELETE, я в растерянности. Кажется, должен быть способ получить преимущества Entity Framework для отображения O / R, но все же вручную управлять операциями внутри транзакции!

Ответы [ 2 ]

2 голосов
/ 02 марта 2012

Должно быть что-то еще, что вы не показываете, потому что нет проблем с тем, как вы присоединяете и добавляете сущности.Следующий код присоединит myA, myC, myB0 и myB1 к контексту как неизменному и установит состояние myC в измененное состояние.

myDbSetC.Attach(myC);
context.Entry(myC).State = Modified;

, следующий код правильно обнаружит, чтовсе сущности уже подключены, и вместо того, чтобы генерировать исключение (как это было бы в ObjectContext API) или вставлять все сущности снова (как вы ожидаете), он просто изменит myB0 и myB1 на добавленное состояние:

myDbSetB.Add(myB0);
myDbSetB.Add(myB1);

Если ваши myA и myC правильно инициализированы с ключами существующих сущностей, весь код будет правильно выполняться и сохраняться, за исключением одной проблемы:

C myC = new C() { 
    A = myA;
}

Это похоже на независимая ассоциация и независимая ассоциация имеет свое собственное состояние, но API для установки его состояния недоступно в DbContext API .Если это новое отношение, которое вы хотите сохранить, оно не будет сохранено, поскольку оно по-прежнему отслеживается как неизменное.Вы должны либо использовать ассоциацию внешнего ключа, либо конвертировать свой контекст в ObjectContext:

ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;

и использовать от ObjectStateManager до , чтобы изменить состояние отношения .

0 голосов
/ 03 марта 2012

Как предположил Ладислав, я получил согласованные экземпляры объекта, что решило проблему с попыткой добавления избыточного As.

Как оказалось, и B0, и B1 фактически инкапсулируют другие объекты (D0 и D1 соответственно), которые в свою очередь инкапсулируют A. И D0, и D1 уже были в базе данных, но не отслеживаются Entity.

Добавление B0 / B1 приводило к ошибочной вставке D0 / D1. Я закончил тем, что использовал API объектного контекста, и Ладислав предложил пометить ObjectStateEntry для D0 / D1 как неизменный, а отношения между D0 / D1 и A как неизменные. Кажется, это делает то, что мне нужно: обновите C и вставьте только B0 / B1.

Ниже приведен мой код для этого, который я вызываю прямо перед SaveChanges. Обратите внимание, что я уверен, что все еще есть некоторые крайние случаи, которые не обрабатываются, и это не проходит тщательное тестирование - но это должно дать грубое представление о том, что нужно сделать.

// Entries are put in here when they are explicitly added, modified, or deleted.
private ISet<DbEntityEntry> trackedEntries = new HashSet<DbEntityEntry>();
private void MarkGraphAsUnchanged()
{
    IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct();
    foreach (DbEntityEntry entry in entriesItWantsToChange)
    {
        if (!this.trackedEntries.Contains(entry))
        {
            entry.State = System.Data.EntityState.Unchanged;
        }
    }

    IEnumerable<ObjectStateEntry> allEntries =
            this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added)
            .Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted))
            .Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified));

        foreach (ObjectStateEntry entry in allEntries)
        {
            if (entry.IsRelationship)
            {
                /* We can't mark relationships are being unchanged if we are truly adding or deleting the entity.
                 * To determine this, we need to first lookup the entity keys, then state entries themselves.
                 */
                EntityKey key1 = null;
                EntityKey key2 = null;
                if (entry.State == EntityState.Deleted)
                {
                    key1 = (EntityKey)entry.OriginalValues[0];
                    key2 = (EntityKey)entry.OriginalValues[1];
                }
                else if (entry.State == EntityState.Added)
                {
                    key1 = (EntityKey)entry.CurrentValues[0];
                    key2 = (EntityKey)entry.CurrentValues[1];
                }

                ObjectStateEntry entry1 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key1);
                ObjectStateEntry entry2 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key2);

                if ((entry1.State != EntityState.Added) && (entry1.State != EntityState.Deleted) && (entry2.State != EntityState.Added) && (entry2.State != EntityState.Deleted))
                {
                    entry.ChangeState(EntityState.Unchanged);
                }
            }
        }
    }

Уф !!! Основной шаблон:

  1. Явно отслеживайте изменения по мере их внесения.
  2. Вернитесь и уберите все, что Entity считает необходимым, но на самом деле не делает.
  3. Фактически сохранить изменения в БД.

Этот метод «вернуться и очистить», очевидно, является неоптимальным, но на данный момент он является наилучшим вариантом без необходимости вручную подключать периферийные объекты (например, D0 / D1), прежде чем я попытаюсь сохранить операцию. Помогает наличие всей этой логики в общем репозитории - логику нужно написать только один раз. Я надеюсь, что в будущем выпуске Entity сможет добавить эту возможность напрямую (и снять ограничение на наличие нескольких экземпляров объекта в куче, но с одним и тем же ключом).

...