Aktualizuj relacje podczas zapisywania zmian obiektów EF4 POCO

107

Entity Framework 4, obiekty POCO i ASP.Net MVC2. Mam wiele relacji, powiedzmy między podmiotami BlogPost i Tag. Oznacza to, że w mojej wygenerowanej przez T4 klasie POCO BlogPost mam:

public virtual ICollection<Tag> Tags {
    // getter and setter with the magic FixupCollection
}
private ICollection<Tag> _tags;

Proszę o BlogPost i powiązane Tagi z instancji ObjectContext i wysyłam je do innej warstwy (Widok w aplikacji MVC). Później otrzymuję zaktualizowany BlogPost ze zmienionymi właściwościami i zmienionymi relacjami. Na przykład miał tagi „A”, „B” i „C”, a nowe tagi to „C” i „D”. W moim przykładzie nie ma nowych tagów, a właściwości tagów nigdy się nie zmieniają, więc jedyną rzeczą, którą należy zapisać, są zmienione relacje. Teraz muszę zapisać to w innym ObjectContext. (Aktualizacja: teraz próbowałem to zrobić w tej samej instancji kontekstowej i również się nie udało).

Problem: nie mogę sprawić, żeby to właściwie uratowało relacje. Próbowałem wszystkiego, co znalazłem:

  • Controller.UpdateModel i Controller.TryUpdateModel nie działają.
  • Pobieranie starego BlogPost z kontekstu, a następnie modyfikowanie kolekcji nie działa. (różnymi metodami od następnego punktu)
  • To prawdopodobnie zadziała, ale mam nadzieję, że to tylko obejście, a nie rozwiązanie :(.
  • Wypróbowano funkcje Attach / Add / ChangeObjectState dla BlogPost i / lub Tagi we wszystkich możliwych kombinacjach. Niepowodzenie.
  • To wygląda co muszę, ale to nie działa (próbowałem to naprawić, ale nie mogę do mojego problemu).
  • Wypróbowano ChangeState / Add / Attach / ... obiekty relacji kontekstu. Niepowodzenie.

„Nie działa” oznacza w większości przypadków, że pracowałem nad danym „rozwiązaniem”, dopóki nie wygeneruje ono żadnych błędów i nie zapisze przynajmniej właściwości BlogPosta. To, co dzieje się z relacjami, jest różne: zwykle Tagi są ponownie dodawane do tabeli Tagów z nowymi PK, a zapisany BlogPost odwołuje się do nich, a nie do oryginalnych. Oczywiście zwrócone tagi mają PK, a przed metodami save / update sprawdzam PK i są one równe tym w bazie danych, więc prawdopodobnie EF myśli, że są to nowe obiekty, a te PK to tymczasowe.

Problem, o którym wiem i może uniemożliwić znalezienie zautomatyzowanego prostego rozwiązania: Kiedy kolekcja obiektu POCO zostanie zmieniona, powinno to nastąpić przez wspomnianą powyżej właściwość kolekcji wirtualnej, ponieważ wtedy sztuczka FixupCollection zaktualizuje odwrotne odwołania na drugim końcu relacji wiele-do-wielu. Jednak gdy widok „zwraca” zaktualizowany obiekt BlogPost, tak się nie stało. Oznacza to, że może nie ma prostego rozwiązania mojego problemu, ale to by mnie bardzo zasmuciło i nienawidziłbym triumfu EF4-POCO-MVC :(. To również oznaczałoby, że EF nie może tego zrobić w środowisku MVC cokolwiek Używane są typy obiektów EF4 :(. Myślę, że śledzenie zmian oparte na migawkach powinno wykryć, że zmieniony BlogPost ma powiązania z tagami z istniejącymi PK.

Btw: Myślę, że ten sam problem występuje w relacjach jeden do wielu (Google i mój kolega tak twierdzą). Spróbuję w domu, ale nawet jeśli to zadziała, nie pomoże mi to w moich sześciu relacjach wiele do wielu w mojej aplikacji :(.

peterfoldi
źródło
Wpisz swój kod. To typowy scenariusz.
John Farrell,
1
Mam automatyczne rozwiązanie tego problemu, jest ukryte w odpowiedziach poniżej, więc wielu by go przegapiło, ale proszę spojrzeć, ponieważ zaoszczędzi ci to piekielnej
pracy.Zobacz
@brentmckendrick Myślę, że inne podejście jest lepsze. Zamiast przesyłać cały zmodyfikowany graf obiektu przez przewód, dlaczego nie zamiast tego po prostu wysłać deltę? W takim przypadku nie potrzebujesz nawet wygenerowanych klas DTO. Jeśli tak czy inaczej masz opinię na ten temat, porozmawiajmy o tym na stackoverflow.com/questions/1344066/calculate-object-delta .
HappyNomad

Odpowiedzi:

145

Spróbujmy w ten sposób:

  • Dołącz BlogPost do kontekstu. Po dołączeniu obiektu do kontekstu stan obiektu, wszystkie powiązane obiekty i wszystkie relacje są ustawiane na Niezmienione.
  • Użyj opcji context.ObjectStateManager.ChangeObjectState, aby ustawić BlogPost na wartość Modified
  • Iteruj po kolekcji znaczników
  • Użyj opcji context.ObjectStateManager.ChangeRelationshipState, aby ustawić stan relacji między bieżącym tagiem a BlogPost.
  • Zapisz zmiany

Edytować:

Myślę, że jeden z moich komentarzy dał ci fałszywą nadzieję, że EF dokona dla ciebie scalenia. Dużo bawiłem się tym problemem i mój wniosek mówi, że EF nie zrobi tego za Ciebie. Myślę, że znalazłeś również moje pytanie na MSDN . W rzeczywistości w Internecie jest mnóstwo takich pytań. Problem w tym, że nie jest jasno określone, jak sobie poradzić z tym scenariuszem. Spójrzmy więc na problem:

Tło problemu

EF musi śledzić zmiany w jednostkach, aby trwałość wiedział, które rekordy muszą zostać zaktualizowane, wstawione lub usunięte. Problem polega na tym, że za śledzenie zmian odpowiedzialny jest ObjectContext. ObjectContext może śledzić zmiany tylko dla dołączonych jednostek. Jednostki, które są tworzone poza ObjectContext, nie są w ogóle śledzone.

Opis problemu

Na podstawie powyższego opisu możemy wyraźnie stwierdzić, że EF jest bardziej odpowiedni dla połączonych scenariuszy, w których jednostka jest zawsze dołączona do kontekstu - typowego dla aplikacji WinForm. Aplikacje internetowe wymagają scenariusza rozłączenia, w którym kontekst jest zamykany po przetworzeniu żądania, a zawartość jednostki jest przekazywana jako odpowiedź HTTP do klienta. Kolejne żądanie HTTP dostarcza zmodyfikowaną zawartość jednostki, która musi zostać odtworzona, dołączona do nowego kontekstu i utrwalona. Rekreacja zwykle odbywa się poza zakresem kontekstu (architektura warstwowa z uporczywą ignorancją).

Rozwiązanie

Jak więc poradzić sobie z takim oderwanym scenariuszem? Korzystając z klas POCO mamy 3 sposoby radzenia sobie ze śledzeniem zmian:

  • Migawka - wymaga tego samego kontekstu = jest bezużyteczna dla scenariusza z rozłączeniem
  • Dynamiczne serwery proxy do śledzenia - wymagają tego samego kontekstu = bezużyteczne w przypadku braku połączenia
  • Synchronizacja ręczna.

Ręczna synchronizacja na pojedynczym obiekcie jest łatwym zadaniem. Wystarczy dołączyć jednostkę i wywołać AddObject w celu wstawienia, DeleteObject w celu usunięcia lub ustawienia stanu w ObjectStateManager na Modified w celu aktualizacji. Prawdziwy ból pojawia się, gdy mamy do czynienia z grafem obiektowym zamiast pojedynczej jednostki. Ten ból jest jeszcze gorszy, gdy masz do czynienia z niezależnymi stowarzyszeniami (takimi, które nie używają własności klucza obcego) i wieloma powiązaniami. W takim przypadku musisz ręcznie zsynchronizować każdą jednostkę w grafie obiektów, ale także każdą relację w grafie obiektów.

Synchronizacja ręczna jest proponowana jako rozwiązanie w dokumentacji MSDN: Dołączanie i odłączanie obiektów mówi:

Obiekty są dołączane do kontekstu obiektu w stanie niezmienionym. Jeśli musisz zmienić stan obiektu lub relacji, ponieważ wiesz, że obiekt został zmodyfikowany w stanie odłączonym, użyj jednej z następujących metod.

Wspomniane metody to ChangeObjectState i ChangeRelationshipState of ObjectStateManager = ręczne śledzenie zmian. Podobna propozycja znajduje się w innym artykule dokumentacji MSDN: Definiowanie i zarządzanie relacjami mówi:

Jeśli pracujesz z odłączonymi obiektami, musisz ręcznie zarządzać synchronizacją.

Ponadto istnieje wpis na blogu dotyczący EF v1, który krytykuje dokładnie to zachowanie EF.

Powód rozwiązania

EF ma wiele "pomocnych" operacji i ustawień, takich jak Refresh , Load , ApplyCurrentValues , ApplyOriginalValues , MergeOption itp. Jednak według moich badań wszystkie te funkcje działają tylko dla pojedynczej encji i wpływają tylko na właściwości skalarne (= nie właściwości nawigacji i relacje). Raczej nie testuję tych metod ze złożonymi typami zagnieżdżonymi w encji.

Inne proponowane rozwiązanie

Zamiast prawdziwej funkcjonalności scalania, zespół EF zapewnia coś, co nazywa się jednostkami samośledzącymi (STE), które nie rozwiązują problemu. Przede wszystkim STE działa tylko wtedy, gdy ta sama instancja jest używana do całego przetwarzania. W przypadku aplikacji webowej tak nie jest, chyba że przechowujesz instancję w stanie widoku lub sesji. Z tego powodu jestem bardzo niezadowolony z używania EF i zamierzam sprawdzić cechy NHibernate. Pierwsza obserwacja mówi, że NHibernate może mieć taką funkcjonalność .

Wniosek

Skończyłem na tym założeniu jednym linkiem do innego pokrewnego pytania na forum MSDN. Sprawdź odpowiedź Zeeshana Hirani. Jest autorem Entity Framework 4.0 Recipes . Jeśli mówi, że automatyczne scalanie grafów obiektów nie jest obsługiwane, wierzę mu.

Ale nadal istnieje możliwość, że całkowicie się mylę i istnieją pewne funkcje automatycznego scalania w EF.

Edycja 2:

Jak widać, zostało to już dodane do MS Connect jako sugestia w 2007 roku. MS zamknęło to jako coś do zrobienia w następnej wersji, ale tak naprawdę nic nie zostało zrobione, aby poprawić tę lukę poza STE.

Ladislav Mrnka
źródło
7
To jedna z najlepszych odpowiedzi, jakie przeczytałem w SO. Wyraźnie określiłeś, jakich artykułów MSDN, dokumentacji i postów na blogu na ten temat nie udało się przekazać. EF4 z natury nie obsługuje aktualizowania relacji z „odłączonych” jednostek. Zapewnia tylko narzędzia do samodzielnego wdrożenia. Dziękuję Ci!
tyriker
1
A co powiesz na NHibernate związanego z tym problemem w porównaniu z EF4?
CallMeLaNN
1
Jest to bardzo dobrze obsługiwane w NHibernate :-) nie ma potrzeby ręcznego scalania, w moim przykładzie jest to 3-poziomowy, głęboki wykres obiektów, pytanie ma odpowiedzi, każda odpowiedź ma komentarze, a pytanie ma również komentarze. NHibernate może utrwalić / scalić graf obiektu, bez względu na to, jak bardzo jest skomplikowany ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Inny zadowolony użytkownik NHibernate: codinginstinct.com/2009/11/…
Michael Buen,
2
Jedno z najlepszych wyjaśnień, jakie kiedykolwiek czytałem !!
Wielkie
2
Zespół EF planuje zająć się tym post-EF6. Możesz zagłosować na entityframework.codeplex.com/workitem/864
Eric J.
19

Mam rozwiązanie problemu, które opisał wyżej Ladislav. Utworzyłem metodę rozszerzenia dla DbContext, która automatycznie wykona dodawanie / aktualizację / usuwanie na podstawie różnicy podanego wykresu i utrwalonego wykresu.

Obecnie korzystając z Entity Framework będziesz musiał ręcznie wykonać aktualizacje kontaktów, sprawdzić, czy każdy kontakt jest nowy i dodać, sprawdzić, czy został zaktualizowany i edytować, sprawdzić, czy został usunięty, a następnie usunąć go z bazy danych. Kiedy będziesz musiał to zrobić dla kilku różnych agregatów w dużym systemie, zaczynasz zdawać sobie sprawę, że musi istnieć lepszy, bardziej ogólny sposób.

Sprawdź, czy może pomóc http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a- wykres-odłączonych-bytów /

Możesz przejść bezpośrednio do kodu tutaj https://github.com/refactorthis/GraphDiff

brentmckendrick
źródło
Jestem pewien, że możesz łatwo rozwiązać to pytanie, źle się z tym bawię.
Shimmy Weitzhandler
1
Cześć Shimmy, przepraszam, w końcu mam trochę czasu, żeby spojrzeć. Dziś wieczorem się tym zajmę.
brentmckendrick
Ta biblioteka jest świetna i zaoszczędziła mi TAK dużo czasu! Dzięki!
lordjeb
9

Wiem, że jest późno na OP, ale ponieważ jest to bardzo częsty problem, opublikowałem to na wypadek, gdyby służyło komuś innemu. Bawiłem się tym problemem i myślę, że znalazłem dość proste rozwiązanie, co robię:

  1. Zapisz główny obiekt (na przykład blogi), ustawiając jego stan na Zmodyfikowany.
  2. Zapytaj bazę danych o zaktualizowany obiekt, w tym kolekcje, które muszę zaktualizować.
  3. Zapytaj i przekonwertuj .ToList () encje, które mają zawierać moja kolekcja.
  4. Zaktualizuj kolekcje głównego obiektu do listy, którą otrzymałem z kroku 3.
  5. Zapisz zmiany();

W poniższym przykładzie „dataobj” i „_categories” to parametry odebrane przez mój kontroler, „dataobj” to mój główny obiekt, a „_categories” to IEnumerable zawierające identyfikatory kategorii wybranych przez użytkownika w widoku.

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

Działa nawet dla wielu relacji

c0y0teX
źródło
7

Zespół Entity Framework zdaje sobie sprawę, że jest to problem z użytecznością i planuje rozwiązać go po EF6.

Od zespołu Entity Framework:

Jest to problem użyteczności, którego jesteśmy świadomi i o którym myśleliśmy i planujemy więcej pracy nad post-EF6. Utworzyłem ten element pracy, aby śledzić problem: http://entityframework.codeplex.com/workitem/864 Element pracy zawiera również łącze do elementu głosowego użytkownika dotyczącego tego - zachęcam do głosowania na niego, jeśli masz jeszcze tego nie zrobiłem.

Jeśli ma to na Ciebie wpływ, zagłosuj na tę funkcję pod adresem

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

Eric J.
źródło
po EF6? który to będzie rok w optymistycznym przypadku?
quetzalcoatl
@quetzalcoatl: Przynajmniej jest to na ich radarze :-) EF przeszedł długą drogę od EF 1, ale wciąż ma przed sobą wiele.
Eric J.
1

Wszystkie odpowiedzi świetnie wyjaśniły problem, ale żadna z nich tak naprawdę nie rozwiązała problemu.

Odkryłem, że jeśli nie użyję relacji w encji nadrzędnej, ale po prostu dodam i usunę jednostki podrzędne, wszystko działa dobrze.

Przepraszam za VB, ale w tym właśnie jest napisany projekt, nad którym pracuję.

Jednostka nadrzędna „Report” ma relację jeden do wielu z „ReportRole” i ma właściwość „ReportRoles”. Nowe role są przekazywane przez oddzielony przecinkami ciąg z wywołania Ajax.

Pierwsza linia usunie wszystkie jednostki podrzędne, a jeśli użyję „report.ReportRoles.Remove (f)” zamiast „db.ReportRoles.Remove (f)”, otrzymam błąd.

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}))
Alan Bridges
źródło