Обновите отношения при сохранении изменений объектов POCO EF4 - PullRequest
107 голосов
/ 03 сентября 2010

Entity Framework 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;

Я запрашиваю BlogPost и связанные теги из экземпляра ObjectContext и отправляю его на другой слой (просмотр в приложении MVC). Позже я возвращаю обновленный BlogPost с измененными свойствами и измененными отношениями. Например, у него были теги «A», «B» и «C», а новые теги - «C» и «D». В моем конкретном примере нет новых тегов, и свойства тегов никогда не меняются, поэтому единственное, что следует сохранить, - это измененные отношения. Теперь мне нужно сохранить это в другом ObjectContext. (Обновление: теперь я пытался сделать в том же контексте экземпляр и тоже не удалось.)

Проблема: я не могу сохранить отношения должным образом. Я перепробовал все, что нашел:

  • Controller.UpdateModel и Controller.TryUpdateModel не работают.
  • Получение старого BlogPost из контекста с последующим изменением коллекции не работает. (другими методами из следующего пункта)
  • Это , вероятно, будет работать, но я надеюсь, что это просто обходной путь, а не решение: (.
  • Пробовал Attach / Add / ChangeObjectState функции для BlogPost и / или тегов во всех возможных комбинациях. Не удалось.
  • Это выглядит как то, что мне нужно, но оно не работает (я пытался это исправить, но не могу решить мою проблему).
  • Пробовал ChangeState / Add / Attach / ... объекты отношений контекста. Не удалось.

«Не работает» в большинстве случаев означает, что я работал над данным «решением», пока он не выдаст ошибок и не сохранит хотя бы свойства BlogPost. То, что происходит с отношениями, меняется: обычно теги снова добавляются в таблицу тегов с новыми PK, и сохраненный BlogPost ссылается на них, а не на исходные. Конечно, возвращаемые теги имеют PK, и перед методами сохранения / обновления я проверяю PK, и они совпадают с теми, что в базе данных, поэтому, вероятно, EF считает, что они являются новыми объектами, а эти PK временными.

Проблема, о которой я знаю и которая может сделать невозможным найти простое автоматическое решение: при изменении коллекции объекта POCO это должно происходить с помощью вышеупомянутого свойства виртуальной коллекции, потому что тогда трюк FixupCollection обновит обратные ссылки на другой конец отношения многих ко многим. Однако, когда View «возвращает» обновленный объект BlogPost, этого не произошло. Это означает, что, возможно, не существует простого решения моей проблемы, но это очень огорчило бы меня, и я бы ненавидел триумф EF4-POCO-MVC :(. Это также означало бы, что EF не может делать это в среде MVC, в зависимости от того, что Используются типы объектов EF4 :(. Я думаю, что отслеживание изменений на основе снимка должно выяснить, что измененный BlogPost связан с тегами с существующими PK.

Кстати: я думаю, что та же самая проблема происходит с отношениями один-ко-многим (Google и мой коллега так говорят). Я попробую дома, но даже если это сработает, это не поможет мне в моих шести отношениях «многие ко многим» в моем приложении: (.

Ответы [ 5 ]

143 голосов
/ 03 сентября 2010

Давайте попробуем это так:

  • Присоедините BlogPost к контексту. После присоединения объекта к контексту состояние объекта, всех связанных объектов и всех отношений устанавливается равным Не изменено.
  • Используйте context.ObjectStateManager.ChangeObjectState, чтобы установить для вашего BlogPost значение Modified
  • Итерация по коллекции тегов
  • Используйте context.ObjectStateManager.ChangeRelationshipState, чтобы установить состояние для связи между текущим тегом и BlogPost.
  • SaveChanges

Edit:

Полагаю, один из моих комментариев дал вам ложную надежду, что EF сделает слияние за вас. Я много играл с этой проблемой, и мой вывод гласит, что EF не сделает этого за вас. Я думаю, вы также нашли мой вопрос на MSDN . На самом деле таких вопросов в Интернете достаточно. Проблема в том, что неясно указано, как бороться с этим сценарием. Итак, давайте посмотрим на проблему:

проблемный фон

EF необходимо отслеживать изменения в сущностях, чтобы персистентность знала, какие записи необходимо обновить, вставить или удалить. Проблема заключается в том, что ObjectContext отвечает за отслеживание изменений. ObjectContext может отслеживать изменения только для прикрепленных объектов. Объекты, созданные вне ObjectContext, вообще не отслеживаются.

Описание проблемы

На основании приведенного выше описания мы можем четко заявить, что EF больше подходит для связанных сценариев, где сущность всегда привязана к контексту - типично для приложения WinForm. Для веб-приложений требуется отключенный сценарий, когда контекст закрывается после обработки запроса, а содержимое объекта передается как HTTP-ответ клиенту. Следующий HTTP-запрос предоставляет измененный контент объекта, который должен быть воссоздан, присоединен к новому контексту и сохранен. Отдых обычно происходит вне контекста контекста (многоуровневая архитектура с постоянным невежеством).

Решение

Так, как бороться с таким отключенным сценарием? При использовании классов POCO у нас есть 3 способа отслеживания изменений:

  • Снимок - требуется тот же контекст = бесполезно для отключенного сценария
  • Прокси-серверы динамического отслеживания - требуется тот же контекст = бесполезно для отключенного сценария
  • Ручная синхронизация.

Ручная синхронизация на одном объекте - простая задача. Вам просто нужно присоединить сущность и вызвать AddObject для вставки, DeleteObject для удаления или установить состояние в ObjectStateManager в значение Modified для обновления. Настоящая боль возникает, когда вам приходится иметь дело с графом объектов, а не с одной сущностью. Эта боль еще хуже, когда вам приходится иметь дело с независимыми ассоциациями (теми, которые не используют свойство внешнего ключа) и отношениями многие ко многим. В этом случае вы должны вручную синхронизировать каждую сущность в графе объектов, а также каждое отношение в графе объектов.

Ручная синхронизация предлагается в качестве решения в документации MSDN: Присоединение и отсоединение объектов говорит:

Объекты прикреплены к объекту контекст в неизменном состоянии. если ты нужно изменить состояние объекта или отношения, потому что вы знаете, что ваш объект был изменен в отдельное состояние, используйте один из следующие методы.

Упомянутыми методами являются ChangeObjectState и ChangeRelationshipState из ObjectStateManager = отслеживание изменений вручную. Аналогичное предложение содержится в другой статье документации MSDN: Определение и управление отношениями говорит:

Если вы работаете с отключенным объекты, которые вы должны вручную управлять синхронизации.

Более того, в блоге 1068 *, связанном с EF v1, содержится критическая информация о поведении EF.

Причина решения

EF имеет множество «полезных» операций и настроек, таких как Обновить , Загрузить , ApplyCurrentValues ​​, ApplyOriginalValues ​​, MergeOption и т. Д. Но по моим исследованиям все эти функции работают только для одного объекта и влияют только на скалярные свойства (= не навигационные свойства и отношения). Я скорее не тестирую эти методы со сложными типами, вложенными в сущность.

Другое предлагаемое решение

Вместо реальной функциональности слияния команда EF предоставляет то, что называется Самопроверяемые объекты (STE), которые не решают проблему. Прежде всего, STE работает, только если один и тот же экземпляр используется для всей обработки. В веб-приложении это не так, если вы не сохраняете экземпляр в состоянии просмотра или сеанса. В связи с этим я очень недоволен использованием EF и собираюсь проверить возможности NHibernate. Первое наблюдение говорит о том, что NHibernate, возможно, обладает такой функциональностью .

Заключение

Я закончу эти предположения одной ссылкой на другой вопрос на форуме MSDN. Проверьте ответ Зеешана Хирани. Он является автором Entity Framework 4.0 Recipes . Если он говорит, что автоматическое объединение графов объектов не поддерживается, я верю ему.

Но все же есть вероятность, что я совершенно не прав, и в EF есть некоторые функции автоматического слияния.

Редактировать 2:

Как вы можете видеть, это было уже добавлено в MS Connect как предложение в 2007 году. MS закрыла его как нечто, что должно быть сделано в следующей версии, но на самом деле ничего не было сделано для улучшения этого пробела, кроме STE.

19 голосов
/ 11 декабря 2012

У меня есть решение проблемы, описанной выше Ладиславом.Я создал метод расширения для DbContext, который будет автоматически выполнять добавление / обновление / удаление на основе различий предоставленного графика и постоянного графика.

В настоящее время с использованием Entity Framework вам потребуетсявыполнить обновления контактов вручную, проверить, является ли каждый контакт новым, и добавить, проверить, обновлены ли и отредактированы, проверить, удалены ли, а затем удалить его из базы данных.Как только вам придется сделать это для нескольких различных агрегатов в большой системе, вы начинаете понимать, что должен быть лучший, более общий способ.

Пожалуйста, посмотрите, может ли это помочь http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a-graph-of-detached-entities/

Вы можете перейти прямо к коду здесь https://github.com/refactorthis/GraphDiff

9 голосов
/ 06 июня 2012

Я знаю, что уже поздно для OP, но так как это очень распространенная проблема, я разместил ее на тот случай, если она послужит кому-то еще.Я занимаюсь этой проблемой и думаю, что нашел довольно простое решение, которое я делаю так:

  1. Сохранить основной объект (например, блоги), установив его состояние в значение «Изменено».
  2. Запрос к базе данных для обновленного объекта, включая коллекции, которые мне нужно обновить.
  3. Запрос и преобразование .ToList () сущностей, которые я хочу включить в мою коллекцию.
  4. Обновление основногонабор (ы) объекта в Список, полученный с шага 3.
  5. SaveChanges ();

В следующем примере «dataobj» и «_categories» - это параметры, полученные моимконтроллер «dataobj» - это мой основной объект, а «_categories» - это IEnumerable, содержащий идентификаторы категорий, выбранных пользователем в представлении.

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

Он даже работает для нескольких отношений

7 голосов
/ 12 февраля 2013

Команда Entity Framework знает, что это проблема с удобством использования, и планирует решить ее после EF6.

От команды Entity Framework:

Это проблема юзабилити, о которой мы знаем, и о которой мы думаем и планируем проделать дополнительную работу после EF6. Я создал этот рабочий элемент для отслеживания проблемы: http://entityframework.codeplex.com/workitem/864 Рабочий элемент также содержит ссылку на голосовой элемент пользователя для этого - я рекомендую вам проголосовать за него, если вы еще этого не сделали.

Если это повлияет на вас, проголосуйте за функцию на

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

1 голос
/ 03 апреля 2018

Все ответы были замечательными, чтобы объяснить проблему, но ни один из них не решил для меня проблему.

Я обнаружил, что если я не использовал отношения в родительской сущности, а просто добавил и удалил дочерние сущности, все работало просто отлично.

Извините за VB, но для этого и написан проект, в котором я работаю.

Родительский объект «Отчет» имеет отношение «один ко многим» к «ReportRole» и имеет свойство «ReportRoles». Новые роли передаются в виде строки, разделенной запятыми, из вызова Ajax.

В первой строке будут удалены все дочерние объекты, и если бы я использовал «report.ReportRoles.Remove (f)» вместо «db.ReportRoles.Remove (f)», я получил бы ошибку.

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