Acg N. .
Temat: Repozytorium obiektów (encji)
Ogólnie i niezbyt precyzyjnie - wzorzec zapewnia dostęp do puli obiektów biznesowych (encji) przy ukryciu warstwy (dostępu do danych), która te obiekty tworzy i zwraca.Poniżej wypunktowałem garść informacji związanych ze wzorcem repozytorium.
1. W warstwie modelu mamy jedynie obiekty reprezentujące nasze dane - zero logiki je tworzącej, zero operacji bazodanowych. Mogą być one np. w standardzie POJO/POCO, co bardzo ułatwia przesyłanie ich między warstwami (DTO). Czyste pole, właściwości i kolekcje (puryści unikają nawet pól wyliczanych i jakichkolwiek metod) i obowiązkowo żadnych odniesień do jakiegokolwiek providera danych, np. atrybutów (adnotacji) mapujących, odniesień do sesji, połączeń, etc. Pure data.
2. Nie interesuje nas, czy obiekty te zostaną utworzone przez jakiś ORM (np. NHibernate, Entity Framework, Sooda czy inne), czy też będą tworzone "na piechotę" z "selektów". I tym bardziej nie interesuje nas, jaka to będzie baza danych (bo to dba akurat warstwa ORMa) - od XML przez SQLite po Oracle. Już sam ORM (lub inny provider) separuje nas od dostawcy bazy danych, ale my potrzebujemy jeszcze jedną warstwę - odseparowania od ORMa. Bo jednak może się zdarzyć, że konkretny ORM "dogaduje się" tylko z kilkoma bazami danych, z których akurat żadna nam, z różnych względów, nie pasuje. Albo, z różnych względów, ORMa zastosować nie chcemy.
3. Z p.2 wynika, że jeśli w projekcie zajdzie potrzeba wymiany bazy danych, bądź ORMa na inny, nie trzeba będzie ruszać naszego modelu - i niczego "wzwyż" (kontrolerów/prezenterów, widoków). Mówiąc obrazowo, "goście od "domain model" mogą iść w tym czasie na urlop". Warto to podkreślić, bo to jest główna zaleta repozytorium.
PS: w tym momencie ktoś może zwrócić uwagę, że ORM stanowi przecież repozytorium obiektów. Owszem, stanowi, ale jest zbyt silnie powiązany z konkretnym silnikiem bazy danych. Poza tym, jak już wspomniałem, nie zawsze chcemy z niego korzystać, np. ze względów licencyjnych, bezpieczeństwa, wydajności, umiejętności zespołu, wymagań klienta potrzebna będzie jego wymiana. Trudno teraz rzeźbić po klasach modelu, przerabiając środowisko po Hibernate na środowisko np. Entity Framework, albo na "czyste selekty" i DataAdaptery czy nawet DataReadery (.NET).
4. Każde repozytorium, związane z daną encją, będzie posiadać standardowy interfejs CRUD:
* Create (zapisz dane do bazy),
* Read (odczytaj dane z bazy),
* Update (uaktualnij dane w bazie),
* Delete (usuń dane z bazy).
Wszystko powyżej repozytorium (logika biznesowa, widoki) widzieć będą jedynie te operacje.
----------
I tutaj kończą się w zasadzie informacje o repozytorium. Diagramy UML i przykładowe implementacje są dostępne w sieci. Natomiast z repozytorium wiąże się nierozerwalnie kilka zasadniczych kwestii i dodatkowe wzorce, o których też należy wspomnieć.
----------
5. Problem pojawia się w momencie pytania o implementację operacji "Read". Przyjrzyjmy się mu bliżej. W przypadku usunięcia, update'u czy utworzenia danych wystarczy w argumencie odpowiedniej metody (C/U/D) przekazać po prostu żądany obiekt (a po "drugiej stronie" przykładowo Hibernate już będzie wiedział, jak się nim zająć).
Ale jak zadać zapytanie w sposób inny, niż "query by example" (dla którego przekazanie obiektu-przykładu by wystarczyło; jednak samo QeE to nieco biednie...), nie wiedząc, "kto stoi po stronie dostawcy danych"? Nie możemy użyć HQL, czy Criteria (bo wiążemy się z Hibernate), nie możemy użyć SQL, no bo którego? Owszem, można przyjąć jakiś standard, np. dawny 92, nowszy 2003, bez ŻADNYCH rozszerzeń. Jednak nawet to może być zbyt wiele, bo przecież te standardy są bardzo rozbudowane, a co, jeśli providerem jest np. ODBC Excela? Nie wiemy, "kto odbierze" nasze zapytanie i jak je zrozumie (jak szeroki zakres zapytań rozumie np. "czytnik XMLa").
W ramach implementacji funkcjonalności Read stosowane są różne podejścia, które starają się wyeliminować z warstwy logiki biznesowej takie "engine dedicated queries". Np. klasa repozytorium dziedziczy po interfejsie CRUD zawierającym listę kilku(dziesiesięciu) metod odpytujących o konkretne rzeczy, np. "średnia zarobków pracowników pomiędzy dwiema datami". Interfejs ten jest implementowany przez providera danych, który generuje już konkretne zapytania. Zauważmy, że w tej sytuacji nie ma możliwości zbytniego wpływu na zapytanie. Można jedynie do pewnego stopnia parametryzować taką metodę odpytującą. Często jednak nie obejdzie się bez rekompilacji kodu przy zmianie zapytania po stronie modelu (np. nie średnia ogólna, ale w grupach stażu pracy).
6. Można sobie poradzić z tym problemem wykorzystując np. technologię LINQ - język zapytań niezależny od sterownika bazy danych i oferujący tylko najprostsze konstrukcje. Sterowników LINQ dla różnych ORMów (a nawet dla XMLa) jest już trochę (ostatnio nawet dla NHibernate, gdzie mamy mapowanie LINQ => Criteria), a dodatkowo można sobie samemu napisać własny translator wyrażeń LINQ na konkretny język zapytań. Nie dość, że uzyskujemy kontrolę typów na etapie pisania kodu, to jeszcze jesteśmy niezależni w dużej mierze od tego, co będzie źródłem danych (LINQ dla NH można zastąpić LINQ dla SQL, Entity Framework, XML, czy własnych struktur). Jeśli na danej platformie nie jest dostępna technologia LINQ (chyba na razie tylko w .NET), pozostaje pisanie sparametryzowanych klas zapytań, albo napisanie czegoś na kształt LINQ. Na jednych platformach, zwłaszcza wyposażonych w mechanizm refleksji (Java, .NET) pójdzie to prosto i uzyskamy gratis kontrolę typów na etapie pisania kodu, na innych - może być ciężej i bez kontroli typów.
Pojawia się zatem bardzo typowy konflikt interesów: odseparowanie od providera danych za cenę uogólnienia języka zapytań i rezygnacji ze specyficznych konstrukcji.
Nasz uniwersalny język zapytań nie będzie oferował bardziej egzotycznych konstrukcji, niż proste odpytywanie z filtrowaniem, być może z zapytaniami zagnieżdżonymi (ale pewnie już tylko po stronie FROM, bo niekoniecznie WHERE (czyli np. EXISTS, IN) czy SELECT ( select (select nazwa from t as t1 where t.id=t1.id) as kolumna from t), być może bez możliwości sortowania danych po agregacji (np. LINQ to NHibernate nie wspiera na razie select/group by/order).
7. Czy to wydajne rozwiązanie? To oczywiście zależy od tego, co robimy. Jeśli projekt jest pisany dla wymagającego klienta (w sensie: duża liczba transakcji, złożone transakcje, bezpieczeństwo dostępu do danych wykluczające jakiekolwiek obce, a już nie daj Boże opensource'owe frameworki, typu Hibernate), typu banki, wojsko, systemy BI, to zapewne pominiemy nawet ORMa i będziemy wprost rzeźbić SQLe, wykorzystując jak najwięcej dobrodziejstw konkretnego silnika bazy danych i optymalizując kod ściśle pod niego. Oczywiście i tutaj będzie potrzebne napisanie swojego DAL (wzorzec Data Access Layer), który nieco odseparuje bazę od modelu.
Ale jeśli piszemy mniej złożone oprogramowanie, np. aplikacja desktopowa, mniej złożony system ERP (uwaga na raportowanie, wymagające nieraz wyrafinowanych sztuczek), serwis internetowy, na który liczba wejść nie będzie porównywalna z portalami informacyjnymi i nie stosujemy bardzo złożonych zapytań, dynamicznego SQLa i innych cudów, albo możemy je przerzucić do procedur wbudowanych (tu kolejna dyskusja za i przeciw), to możemy wprowadzać kolejne warstwy, które - chociaż początkowo będą zniechęcać dodatkowym, sporym narzutem technologicznym, to potem szybko się zwrócą przy zmianie technologii, naprawach i rozszerzaniu systemu, a na pewno w fazie produkcji.
8. Kolejne zagadnienie, to kwestia posiadania dużej liczby repozytoriów (przyjmuje się często zasadę - jedno repozytorium na encję). Do ich "ogarnięcia" i automatycznego zarządzania nimi wykorzystuje się np. wzorzec Service Locator, który automatycznie wyszukuje wszystkie repozytoria, tworzy ich instancje, dodaje do wewnętrznej kolekcji i udostępnia je na żądanie. Świetnie sprawdza się tutaj wzorzec Fabryka (repozytoriów), oparta o klasę szablonową.
Przykład rozwiązania (działającego, co nie znaczy optymalnego :) ): http://www.goldenline.pl/forum/1820750/repozytorium-en...Adrian Olszewski edytował(a) ten post dnia 01.08.10 o godzinie 18:21