Relacje Wiele-Wiele w generycznym repozytorium (Entity Framework)

By | March 18, 2016

Ostatnio wspominałem o pewnym problemie, który spotka każdego chętnego, by skorzystać z generycznego repozytorium używając jednocześnie Entity Framework. Problemem tym jest edycja encji, które zawierają w sobie relacje many-to-many. Sam problem jest banalny gdy mamy oddzielne repozytoria lub korzystamy bezpośrednio z DbContext, ale jeśli chcemy trzymać się generyczności, to musimy nieco zmienić naszą metodę Update.

Problem

Gdy edytujemy nasz obiekt i dokonujemy zapisu za pośrednictwem standardowej metody update, wszystkie zmienione wartości zostają zapisane w bazie. Niestety nie dotyczy to kolekcji, która reprezentuje relacje wiele-wiele. Zacznijmy od encji, która sprawiała problemy.

Jeśli użyjemy standardowej metody Update, to będziemy musieli tłumaczyć kucharzowi, dlaczego nie może zmienić zdania i nagle zacząć dokładać do hamburgerów cebuli.

 

Przyczyna

Przyczyna problemu jest stosunkowo prosta. Po przekazaniu obiektu do widoku, zmodyfikowaniu go i przesłaniu do metody Update repozytorium jest on śledzony przez DbContext, ale elementy jego kolekcji już nie. W związku z tym nasz Product będzie zawierał nasz Ingredient w kolekcji, ale Ingredient nie będzie miał w swojej kolekcji obiektu Product, który modyfikujemy. W przypadku korzystania ze zwykłego repozytorium lub bezpośrednie z DbContextu wystarczyłoby użycie metody Include. Niestety moje rozwiązanie nieco komplikuje sytuację.

Rozwiązanie

Pa jakimś czasie na google trafiłem w końcu na rozwiązanie problemu, które po pewnych modyfikacjach udało mi się dostosować do swoich potrzeb. Przeciążona metoda Update, która służy do obsługi obiektów zawierających relacje many-to-many wygląda następująco.

Metoda przyjmuje 3 parametry. Pierwszym z nich jest referencja do obiektu, na którym chcemy wykonać Update. Drugi z nich to kolekcja, którą ma zawierać, trzecia to nazwa właściwości, która jest modyfikowaną kolekcją w encji, którą aktualizujemy.

 

Aby wszystko działało z każdym możliwym typem kolekcji, drugi parametr metody musi pobierać kolekcję obiektów typu object. W związku z tym, w kolejnej linii musimy ustalić jakie właściwie obiekty trafiły do metody, by móc swobodnie na nich operować.

Zaraz potem w tym miejscu T previous = dbSet.Include(propertyName).First(e => e.ID == entity.ID); tworzymy referencję do poprzedniej (sprzed edycji)wersji obiektu który edytujemy i używając metody Include, o której wspomniałem wcześniej, wczytujemy zawartość jego kolekcji. Po co nam potrzebny stary obiekt, który zaraz i tak zmienimy? W zasadzie po nic, ale jeśli tego nie zrobimy i usuniemy coś z jego kolekcji, to zmiany te nie zostaną zapisane, bo bez tego DbContext nie będzie śledził encji, które były zawarte w poprzedniej wersji obiektu.

Zaraz potem musimy stworzyć listę, która będzie odpowiadała typowi kolekcji, którą zawiera nasz obiekt. Najprostsze rozwiązanie jakie znalazłem to linia var values = ListHelper.CreateList(type);, która korzysta z niewielkiej metody zwracającej listę.

Największe problemy miałem z tym fragmentem kodu, który niekoniecznie chciał ze mną współpracować. Kluczem jest oczywiście zrozumienie tego, co dzieję się w całym wyrażeniu i dlaczego.

 

Otóż dla każdego obiektu z kolekcji updatedSet (drugi parametr naszej metody) znajdujemy odpowiadające mu encje w bazie danych, wyciągamy je z niej i dodajemy do utworzonej chwilę wcześniej listy values. Jej zadaniem jest przechowanie ich do następnego kroku, który kończy całą tą metodę.

 

Pierwsza linia zamienia całą kolekcję obiektu na nową, która jest przechowywana w values. Druga zamienia wszystkie inne właściwości naszej encji na te, które zawiera dostarczona przez nas zmodyfikowana encja.

 

Wnioski

Powyższy problem nie byłby nawet w połowie tak skomplikowany, gdybyśmy korzystali bezpośrednio z DbContextu i pominęli całą zabawę z generycznym repozytorium. Eksperyment ten ma swoją cenę i dobitnie pokazał mi jakim bezsensem naprawdę jest używanie wzorca repozytorium w połączeniu z EF. Odcinamy się w ten sposób od mnóstwa narzędzi, które daje nam ten ORM i jeśli już podjęliśmy decyzję o korzystaniu z niego w swoim projekcie, to dorzucając do niego repozytorium pozbywamy się dostępu do ogromnej ilości funkcjonalności.

Dlatego też bardzo poważnie zastanawiam się nad wyrzuceniem całego repozytorium z projektu. Jestem jeszcze na stosunkowo wczesnym etapie prac, więc nie wymagałoby to aż tak dużo pracy.