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

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


person peterfoldi    schedule 03.09.2010    source источник
comment
Пожалуйста, разместите свой код. Это обычный сценарий.   -  person John Farrell    schedule 03.09.2010
comment
У меня есть автоматическое решение этой проблемы, оно скрыто в ответах ниже, поэтому многие пропустили бы его, но, пожалуйста, посмотрите, это сэкономит вам чертовски много работы см. Сообщение здесь   -  person refactorthis    schedule 27.02.2013
comment
@brentmckendrick Я думаю, что другой подход лучше. Вместо того, чтобы посылать по сети весь измененный граф объектов, почему бы вместо этого просто не послать дельту? В этом случае вам даже не понадобятся сгенерированные классы DTO. Если у вас есть мнение по этому поводу, давайте обсудим его на странице stackoverflow.com/ вопросы / 1344066 / вычислить-объект-дельта.   -  person HappyNomad    schedule 18.11.2013


Ответы (5)


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

  • Прикрепите 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 имеет множество "полезных" операций и настроек, например Обновить, Загрузить, ApplyCurrentValues ​​, ApplyOriginalValues ​​, MergeOption и т.д. Но, согласно моему исследованию, все эти функции работают только для одного объекта и влияют только на скалярные свойства (= не свойства и отношения навигации). Я предпочитаю не тестировать эти методы со сложными типами, вложенными в объект.

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

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

Заключение

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

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

Изменить 2:

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

person Ladislav Mrnka    schedule 03.09.2010
comment
Это не добавит тегов, это хорошо. Добавляет новые отношения, это тоже хорошо. Это не удаляет существующие отношения. Это плохо :(. Using (var context = new Entities.Blog ()) {context.BlogPosts.Attach (BlogPost); context.ObjectStateManager.ChangeObjectState (BlogPost, System.Data.EntityState.Modified); foreach (Tag t in BlogPost .Tags) {context.ObjectStateManager.ChangeRelationshipState (BlogPost, t, c = ›c.Tags, System.Data.EntityState.Added);} context.DetectChanges (); context.SaveChanges ();} - person peterfoldi; 03.09.2010
comment
Это также не удается, когда пользователь снова редактирует тот же BlogPost и пытается сохранить. В нем говорится: объект с таким же ключом уже существует в ObjectStateManager. ObjectStateManager не может отслеживать несколько объектов с одним и тем же ключом. В строке: context.BlogPosts.Attach (BlogPost); - person peterfoldi; 03.09.2010
comment
Но это элементарные принципы работы с ORM. Вы должны указать контекст, что было удалено. Он не удалял неизвестную запись. - person Ladislav Mrnka; 03.09.2010
comment
Вы должны отслеживать изменения. Контекст не знает о них, пока вы не показываете, что было изменено. Если вы хотите пойти проще, вы можете снова явно загрузить весь граф объекта и объединить загруженный и прикрепленный объект с данными из View. - person Ladislav Mrnka; 03.09.2010
comment
Если вы хотите пойти проще, вы можете снова явно загрузить весь граф объекта и объединить загруженный и прикрепленный объект с данными из представления. Это то, что я хочу. Отслеживание изменений на основе снимков выполняется EF, а не мной. Загрузка графика в порядке, так как в контроллере мне все равно нужно будет перейти к базе данных, если я хочу узнать изменения. Не могли бы вы мне подсказать? Я это уже пробовал. Я получил исходный график, а затем попытался применить изменения по-разному, но это не сработало :(. - person peterfoldi; 03.09.2010
comment
Но это элементарные принципы работы с ORM. Вы должны указать контекст, что было удалено. Он не удалял неизвестную запись. Это звучит разумно, за исключением того, что я ожидаю наличия функции, а именно: Вот мой объект BlogPost в его новом состоянии, включая все отношения. Пожалуйста, обновите базу данных соответствующим образом. - person peterfoldi; 03.09.2010
comment
Я добавил описание, потому что вы меня неправильно поняли. Более простое решение с перезагрузкой графа объектов не сделает слияние за вас. Это только позволит вам легко найти, какие отношения новые, а какие удалены. Вам все равно нужно будет установить их состояние. - person Ladislav Mrnka; 04.09.2010
comment
Я понимаю. Мой вывод был аналогичным. И я также решил взглянуть на NHibernate, надеясь, что он может сделать эту самую простую вещь для меня. Спасибо за ответ. - person peterfoldi; 05.09.2010
comment
Это один из лучших ответов, которые я читал на SO. Вы четко заявили о том, что не удалось донести такому количеству статей, документации и сообщений в блогах MSDN по этой теме. EF4 по своей сути не поддерживает обновление отношений из отсоединенных сущностей. Он предоставляет вам только инструменты для самостоятельной реализации. Спасибо! - person tyriker; 17.11.2010
comment
Итак, по прошествии нескольких месяцев, как насчет NHibernate, связанного с этой проблемой, по сравнению с EF4? - person CallMeLaNN; 07.04.2011
comment
@CallMeLaNN: Прошло несколько месяцев, но я был вынужден использовать EF все время из-за политики компании. После этого я сменил работу, но еще не исследовал всю связанную с этим функциональность в NHibernate. В ASP.NET MVC есть один обходной путь - загрузка графика из базы данных и использование TryUpdateModel в контроллере, но это передаст много логики в контроллер. - person Ladislav Mrnka; 05.06.2011
comment
Я оставил EF и использовал Fluent NHibernate, что очень хорошо, потому что не нужно иметь дело с конфигурацией XML. NHibernate также имеет SchemaExport, который будет обновлять db из классов сущностей, кажется, функции по сравнению с EF не сильно отличаются. В настоящее время я не использую rel "многие-ко-многим" в своем проекте, но я тестировал связь раньше и, похоже, у меня нет проблем. Довольно красиво и прямолинейно, просто нужно время, чтобы изучить и построить первый конфиг. Тогда разработка классов eintitiy довольно интересна. Я также использую сеанс для каждого запроса. - person CallMeLaNN; 06.06.2011
comment
Это очень хорошо поддерживается в NHibernate :-) Нет необходимости вручную объединять, в моем примере это трехуровневый глубокий граф объектов, у вопроса есть ответы, у каждого ответа есть комментарии, а у вопроса тоже есть комментарии. NHibernate может сохранять / объединять ваш граф объектов, каким бы сложным он ни был ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Еще один довольный пользователь NHibernate: codinginstinct.com/2009/11/ - person Michael Buen; 09.08.2011
comment
Одно из лучших объяснений, которые я когда-либо читал !! Большое спасибо - person marvelTracker; 14.06.2012
comment
@Ladislav Mrnka, этот вопрос все еще актуален в Entity Framework 4.3 или есть ли альтернативные способы достижения этого в новой версии? - person marvelTracker; 15.06.2012
comment
Похоже, в EF 5 у них все еще есть проблема, спасибо за все публикации и комментарии здесь, это лучшее объяснение, которое я читал по этой проблеме, и я много читал об этом. Спасибо, Ладислав, за то, что всегда был рядом с нашими вопросами EF, ну, я думаю, теперь мы могли бы спросить вас о проблемах NHibernates: P - person RJardines; 25.10.2012
comment
Всем привет. Взгляните на мой ответ ниже, который решает эту проблему в EF. Надеюсь, эта идея будет подхвачена самой командой EF для использования в будущем. - person refactorthis; 11.12.2012
comment
Команда EF планирует решить эту проблему после EF6. Вы можете проголосовать за entityframework.codeplex.com/workitem/864 - person Eric J.; 12.02.2013

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

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

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

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

person refactorthis    schedule 11.12.2012
comment
Я уверен, что вы легко сможете решить этот вопрос, я Мне плохо с этим. - person Shimmy Weitzhandler; 04.07.2013
comment
Привет, Шимми, извини, наконец-то есть время взглянуть. Я изучу это сегодня вечером. - person refactorthis; 13.07.2013
comment
Эта библиотека великолепна и сэкономила мне ТАК много времени! Спасибо! - person lordjeb; 24.03.2015

Я знаю, что уже поздно для 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();

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

person c0y0teX    schedule 06.06.2012

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

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

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

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

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

person Eric J.    schedule 11.02.2013
comment
после EF6? в каком году это будет тогда в оптимистичном случае? - person quetzalcoatl; 13.02.2013
comment
@quetzalcoatl: По крайней мере, это на их радаре :-) EF проделала долгий путь со времен EF 1, но еще есть над чем поработать. - person Eric J.; 13.02.2013

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

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

Извините за 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}))
person Alan Bridges    schedule 03.04.2018