Na etapie projektowania YumYum miałem całkiem poważny dylemat dotyczący wyboru rozwiązania, które będzie łącznikiem aplikacji z bazą danych. Wcześniej używałem do tego wzorca repozytorium, który świetnie się sprawdzał przy niewielkich, treningowych projektach. Ostatnimi czasy przeczytałem mnóstwo głosów twierdzących (całkiem słusznie), że używanie wzorca repozytorium z Enity Frameworkiem nie ma najmniejszego sensu. Początkowo miałem zamiar korzystać bezpośrednio z DbContextu, ale…
Z pewnością chciałeś się czytelniku dowiedzieć, co spowodowało ten nagły zwrot akcji i jeśli zostaniesz ze mną jeszcze minutkę, to z pewnością podzielę się z Tobą tą informacją. Najpierw jednak pragnę uściślić jedną, jedyną rzecz. Nie mam tutaj zamiaru wchodzić w dyskusję i dowodzić wyższości któregokolwiek rozwiązania. Repozytorium z Entity Frameworkiem działa i jest rozwiązaniem bardzo łatwym i przyjemnym, choć nie idealnym i w rzeczywistości faktycznie nie mającym wielkiego sensu. Poniżej przytoczę, krótkie wprowadzenie w dwa tytułowe wzorce, a w następnym poście zajmę się konkretnym powodem, który chciałem eksperymentalnie wypróbować i stąd też moja ostateczna decyzja. Mowa mianowicie o implementacji generycznego repozytorium z wzorcem Unit of Work.
Wzorzec repozytorium
W każdym systemie e-commerce, a nad takim właśnie pracuję, spotkamy się z koniecznością przechowywania danych dotyczących produktów, klientów, adresów, sposobów wysyłki, kodów rabatowych. Dane te muszą być przetwarzane i wyświetlane praktycznie na każdym kroku działania aplikacji. Dostać się do nich i przekazać je w odpowiednie miejsca możemy na kilka różnych sposobów, jednym z nich jest właśnie wzorzec repozytorium.
Jest to jeden z podstawowych i prostszych wzorców projektowych i składa się w zasadzie z samego tylko repozytorium, które w przypadku MVC znajduje się niejako pomiędzy kontrolerami, a bazą danych (u mnie dochodzi do tego DbContext Entity Framework, który jest główną kością niezgody we wspomnianym wcześniej sporze, ale pomińmy ten fakt). Dla potrzeb przykładu przyjmijmy, że nazywa się ono Repository i jest implementacją interfejsu IRepository, a przechowywać będziemy w nim proste obiekty Person.
1 2 3 4 5 |
public class Person { public int Id {get; set; } public string Name {get; set; } } |
Podstawowe repozytorium implementuje mniej interfejs wyglądający mniej więcej w ten sposób.
1 2 3 4 5 6 7 8 |
public interface IRepository { IEnumerable<Person> GetAll {get;} Person Get(int id); void Update(Person entity); void Add(Person entity); void Delete(int id); } |
Oczywiście można w nim zawrzeć więcej mniej lub bardziej skomplikowanych metod, ale nie zawsze jest to konieczne. Jak wygląda implementacja tego interfejsu? Pokażę to na jednej, przykładowej metodzie. Resztę naprawdę bez problemu można wyszukać w Internecie lub choćby wydedukować.
1 2 3 4 5 |
public Person Get(int id) { //context jest instancją klasy DbContext, natomiast context.People odwołuje się do DbSet zawierającego encje Person. return context.People.First(p => p.Id == id); } |
Ta prosta metoda zwraca pierwszy napotkany obiekt, który ma właściwość Id równą tej, którą podano na wejściu. W przykładowym kontrolerze wywołanie tej metody wyglądałoby następująco.
1 2 3 4 |
public ActionResult ShowPerson(int id) { return View(repository.Get(id)); } |
Wszystko to jest bardzo proste, a mockowanie repozytoriów tego typu w testach jest wręcz banalne, o wstrzykiwaniu ich w konstruktorze wolę nie mówić. Co jednak jeśli mamy w swoim projekcie mnóstwo przeróżnych encji, a niektóre kontrolery potrzebują korzystać z kilku różnych repozytoriów. Czy musimy tworzyć mnóstwo instancji repozytoriów, dla każdego z nich tworzyć nowy kontekst i wysyłać duże ilości zapytań do bazy tylko po to, żeby pokazać jakiś wykres w statystykach? Całe szczęście możemy sobie to trochę uprościć, a rozwiązaniem jest…
Unit of Work
W rzeczywistości bezpośrednim celem Unit of Work nie jest pomoc w uporządkowaniu dużej ilości repozytoriów, ale jest to niejako skutkiem ubocznym. Rzeczywistą rolą tego wzorca jest zebranie operacji na kilku repozytoriach i wysłanie ich w formie jednej transakcji do bazy danych. Jest to o wiele mniejsze obciążenie, a przy okazji sprawia, że wśród repozytoriów panuje odrobinę mniejszy bałagan.
Prosty interfejs UoW może wyglądać następująco:
1 2 3 4 5 6 7 |
public interface IUnitOfWork { IRepository Repository1 {get; set;} IRepository Repository2 {get; set;} //etc.. void Save(); } |
Wzorzec UoW najpierw “zbiera” zadania, które chcemy wykonać na jego repozytoriach, składa całość w jedną transakcję i wysyła ją do bazy. Gdybym w swoim projekcie nie korzystał z Entity Framework to:
a) UnitOfWork musiałby zawierać trochę więcej kodu i pełnić trochę większą rolę.
b) Byłby użyteczny.
Prawdziwy, w pełni funkcjonalny Unit of Work można sobie obejrzeć oglądając klasę DbContext, która jest implementacją tego wzorca i pełni wszystkie jego role.
UoW + Repository w Entity Framework
Ja zatem wygląda stosowanie powyższych wzorców z Entity Framework? Jeśli nie chcemy w żaden sposób wzbogacić tego co oferują nam DbContext(Unit of Work) i jego DbSet(Repozytorium), to jedyne co osiągniemy, przy użyciu prostych implementacji, to ograniczymy sobie funkcjonalność jaką dostajemy z EF out of the box. Gdyby nie to, że chciałem pobawić się trochę repozytoriami generycznymi (o których za kilka dni), to zapewne odwoływałbym się bezpośrednio do DbContextu.
Kilka linków:
http://radblog.pl/2016/01/wzorzec-repository-kilka-slow-przeciwko/
http://piotrgankiewicz.com/2016/03/05/repository-so-we-meet-again/