Оновлення взаємозв'язків при збереженні змін об'єктів EF4 POCO


107

Entity Framework 4, об'єкти POCO та ASP.Net MVC2. Я маю багато-багато стосунків, скажімо між суб'єктами BlogPost та Tag. Це означає, що в моєму генерованому Т4 класі 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 з контексту та змінити колекцію не вийде. (різними методами з наступного пункту)
  • Це, мабуть, спрацювало б, але я сподіваюся, що це лише рішення, а не рішення :(.
  • Перевірені функції додавання / додавання / зміниObjectState для BlogPost та / або тегів у всіх можливих комбінаціях. Не вдалося.
  • Це виглядає як те, що мені потрібно, але це не працює (я намагався виправити це, але не можу для своєї проблеми).
  • Спробував ChangeState / Add / Attach / ... об'єкти зв’язку контексту. Не вдалося.

"Не працює" означає, що в більшості випадків я працював над даним "рішенням", поки воно не створює помилок і зберігає принаймні властивості BlogPost. Те, що відбувається із відносинами, змінюється: теги зазвичай додаються знову до таблиці тегів з новими ПК, а збережені BlogPost посилаються на ті, а не на оригінальні. Звичайно, повернуті Теги мають ПК, і перед методами збереження / оновлення я перевіряю ПК, і вони рівні рівним тим, які знаходяться в базі даних, тому, ймовірно, EF вважає, що це нові об'єкти, а ці ПК - тимчасові.

Проблему, про яку я знаю, і яка може унеможливити пошук автоматизованого простого рішення: Коли колекція об'єкта POCO буде змінена, це повинно відбутися вищезгаданим властивістю віртуальної колекції, оскільки тоді фокус FixupCollection оновить зворотні посилання на іншому кінці відносин "багато хто до багатьох" Однак коли View "повертає" оновлений об’єкт BlogPost, цього не сталося. Це означає, що, можливо, немає простого рішення моєї проблеми, але це дуже засмутить мене, і я ненавиджу тріумф EF4-POCO-MVC :(. Також це означатиме, що EF не може цього зробити в середовищі MVC залежно від того Використовуються типи об’єктів EF4 :(. Я думаю, що відстеження змін на основі знімків повинно з’ясувати, що змінений BlogPost має зв'язок із тегами з існуючими ПК.

Btw: Я думаю, що однакова проблема трапляється і з стосунками один на багато (так говорять google і мій колега). Я спробую вдома, але навіть якщо це працює, це не допомагає мені в моїх шести багато-багато-багато стосунків у моєму додатку :(.


Будь ласка, опублікуйте свій код. Це загальний сценарій.
Джон Фаррелл

1
У мене є автоматичне рішення цієї проблеми, вона прихована у відповідях нижче так багато пропустили б це , але, будь ласка, подивіться , як це заощадить вам пекельну роботу см пост тут
brentmckendrick

@brentmckendrick Я думаю, що інший підхід кращий. Замість того, щоб надіслати весь модифікований графік об'єкта по дроту, чому б просто не надіслати дельту? У цьому випадку вам навіть не знадобляться згенеровані класи DTO. Якщо у вас є думка про це в будь-якому випадку, будь ласка, обговоримо це на сайті stackoverflow.com/questions/1344066/calculate-object-delta .
HappyNomad

Відповіді:


145

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

  • Приєднайте BlogPost до контексту. Після приєднання об'єкта до контексту стан об'єкта, усіх пов'язаних об'єктів і всіх відносин встановлюється у незмінний.
  • Використовуйте контекст.ObjectStateManager.ChangeObjectState, щоб встановити свій BlogPost для модифікованого
  • Ітерація за допомогою колекції тегів
  • Використовуйте контекст.ObjectStateManager.ChangeRelationshipState, щоб встановити стан для зв'язку між поточним тегом і BlogPost.
  • Зберегти зміни

Редагувати:

Я думаю, один із моїх коментарів дав вам помилкову надію, що EF зробить злиття для вас. Я багато грав з цією проблемою, і мій висновок говорить, що EF не зробить цього для вас. Я думаю, ви також знайшли моє запитання щодо MSDN . Насправді таких питань в Інтернеті багато. Проблема полягає в тому, що чітко не вказано, як боротися з цим сценарієм. Тож давайте розглянемо проблему:

Фон проблеми

EF має відслідковувати зміни сутностей, щоб стійкість знала, які записи потрібно оновлювати, вставляти чи видаляти. Проблема полягає в тому, що за відстеження змін покладається ObjectContext. ObjectContext може відслідковувати зміни лише для доданих об'єктів. Суб'єкти, створені поза ObjectContext, взагалі не відслідковуються.

Опис проблеми

Виходячи з вищеописаного опису, ми можемо чітко стверджувати, що EF більше підходить для підключених сценаріїв, коли сутність завжди прив’язана до контексту - типового для програми WinForm. Веб-додатки вимагають відключеного сценарію, коли контекст закривається після обробки запиту, а вміст сутності передається клієнтові як відповідь HTTP. Наступний HTTP-запит надає змінений вміст об'єкта, який потрібно відтворити, приєднати до нового контексту та зберегти. Відпочинок зазвичай відбувається поза рамками контексту (шарувата архітектура з ігноруванням стійкості).

Рішення

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

  • Знімок - потрібен той же контекст = марний для відключеного сценарію
  • Динамічний проксі-сервер для відстеження - вимагає того ж контексту = марний для відключеного сценарію
  • Ручна синхронізація.

Ручна синхронізація на одному об'єкті - це легке завдання. Вам просто потрібно приєднати сутність і викликати AddObject для вставки, DeleteObject для видалення або встановити стан в ObjectStateManager для модифікації для оновлення. Справжній біль настає, коли вам доведеться мати справу з об'єктним графіком замість однієї сутності. Цей біль ще гірший, коли доводиться стикатися з незалежними асоціаціями (тими, що не використовують майно іноземних ключів) і багатьма відносинами. У такому випадку вам потрібно вручну синхронізувати кожну сутність в об’єктному графіку, а також кожне відношення в об'єктному графіку.

Ручна синхронізація пропонується як рішення документацією 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
Отже, як минуло кілька місяців, як щодо НСЗ, пов'язаного з цим питанням, порівняно з EF4?
CallMeLaNN

1
Це дуже добре підтримується в NHibernate :-) немає необхідності вручну зливатись, в моєму прикладі це трирівневий глибокий об’єктний графік, питання має відповіді, у кожній відповіді є коментарі, а в питанні є також коментарі. NHibernate може зберігати / об'єднувати ваш об’єктний графік, незалежно від того, наскільки він складний ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Ще один задоволений користувач NHibernate: codinginstinct.com/2009/11/…
Michael Буен

2
Одне з найкращих пояснень, які я коли-небудь читав !! Велике спасибі
marvelTracker

2
Команда EF планує вирішити цю проблему після EF6. Ви можете проголосувати за entitframework.codeplex.com/workitem/864
Ерік Дж.

19

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

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

Погляньте і перевірте, чи може це допомогти http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a- графік-відокремлених утворень /

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


Я впевнений, що ви можете легко вирішити це питання, я з ним погано переживаю.
Шиммі Вайцхандлер

1
Привіт Шіммі, вибач, нарешті знайшов деякий час, щоб подивитися. Я загляну в це сьогодні ввечері.
brentmckendrick

Ця бібліотека чудова і заощадила мене так багато часу! Дякую!
lordjeb

9

Я знаю, що вже пізно для ОП, але оскільки це дуже поширене питання, я розмістив це, якщо він служить комусь іншому. Я займався цим питанням і, думаю, що отримав досить просте рішення, що я роблю:

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

Це навіть працює для безлічі відносин


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, але саме так написаний проект, над яким я працюю.

Батьківський об'єкт "Звіт" має відношення до "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}))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.