Обновлять отношения при сохранении изменений объектов EF4 POCO

107

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 и мой коллега). Я попробую дома, но даже если это сработает, это не поможет мне в моих шести отношениях «многие ко многим» в моем приложении :(.

Петерфольди
источник
Пожалуйста, разместите свой код. Это обычный сценарий.
Джон Фаррелл
1
У меня есть автоматическое решение этой проблемы, оно скрыто в ответах ниже, поэтому многие пропустят его, но, пожалуйста, взгляните, так как это сэкономит вам чертовски много работы см. Сообщение здесь
brentmckendrick
@brentmckendrick Я думаю, что другой подход лучше. Вместо того, чтобы отправлять весь измененный граф объектов по сети, почему бы просто не отправить дельту? В этом случае вам даже не понадобятся сгенерированные классы DTO. Если у вас есть мнение по этому поводу, давайте обсудим это на stackoverflow.com/questions/1344066/calculate-object-delta .
HappyNomad

Ответы:

145

Попробуем так:

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

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

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

Фон проблемы

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

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

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

Решение

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

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

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

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

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

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

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

Более того, есть сообщение в блоге, связанное с EF v1, в котором критикуется именно такое поведение EF.

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

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

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

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

Вывод

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

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

Изменить 2:

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

Ладислав Мрнка
источник
7
Это один из лучших ответов, которые я читал на SO. Вы четко заявили, чего не удалось донести так много статей, документации и сообщений в блогах MSDN по этой теме. EF4 по сути не поддерживает обновление отношений из «отсоединенных» сущностей. Он предоставляет вам только инструменты для самостоятельной реализации. Спасибо!
Tyriker
1
Итак, по прошествии нескольких месяцев, как насчет NHibernate, связанного с этой проблемой, по сравнению с EF4?
CallMeLaNN
1
Это очень хорошо поддерживается в NHibernate :-) Нет необходимости вручную объединять, в моем примере это трехуровневый глубокий граф объектов, у вопроса есть ответы, у каждого ответа есть комментарии, а у вопроса тоже есть комментарии. NHibernate может сохранять / объединять ваш граф объектов, независимо от того, насколько он сложен ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Еще один довольный пользователь NHibernate: codinginstinct.com/2009/11/…
Майкл Буэн,
2
Одно из лучших объяснений, которые я когда-либо читал !! Большое спасибо
marvelTracker
2
Команда EF планирует решить эту проблему после EF6. Вы можете проголосовать за entityframework.codeplex.com/workitem/864
Эрик Дж.
19

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

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

Пожалуйста, посмотрите, может ли это помочь http://refactorthis.wordpress.com/2012/12/11/introduction-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a- граф-отдельных-сущностей /

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

Brentmckendrick
источник
Я уверен, что ты легко справишься с этим вопросом, мне с этим не по себе.
Shimmy Weitzhandler
1
Привет, Шимми, извини, наконец-то есть время взглянуть. Я изучу это сегодня вечером.
brentmckendrick
Эта библиотека великолепна и сэкономила мне ТАК много времени! Спасибо!
lordjeb
9

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

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

В следующем примере «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();

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

c0y0teX
источник
7

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

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

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

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

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

Эрик Дж.
источник
после EF6? в каком году это будет тогда в оптимистичном случае?
quetzalcoatl
@quetzalcoatl: По крайней мере, это на их радарах :-) EF прошла долгий путь со времен EF 1, но еще есть над чем поработать.
Эрик Дж.
1

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

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

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

Родительская сущность «Report» имеет отношение «один ко многим» с «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}))
Алан Бриджес
источник