21 December 2010

Mockowanie przy użyciu Spring i Mockito

W poniższym wpisie chciałbym przedstawić jak efektywnie skonfigurować testy integracyjne w Spring z użyciem mocków.

Testy integracyjne w Spring


Tworzenie i uruchamianie testów integracyjnych w Spring jest dziecinnie proste dzięki dobrodziejstwom jakie dostarcza Spring TestContext Framework.
Zakładając, że plik xml konfiguracji kontekstu aplikacji Spring (Spring Application Context) w module aplikacji, który chcemy testować, nazwaliśmy applicationContext.xml, uruchomienie testu integracyjnego JUnit dla takiego moduły wygląda następująco:



Jest to najprostszy sposób na przetestowanie poprawności pliku konfiguracji kontekstu aplikacji, w którym zdefiniowane są zależności pomiędzy klasami zarządzanymi przez Spring.

No dobrze, ale chcemy przetestować konkretny serwis, który stworzyliśmy. Zatem do naszego testu wstrzykujemy serwis i tworzymy dla niego metodę testującą:



Tworzenie mocka


Nasz serwis korzysta z kilku innych serwisów, w tym z serwisu HttpClient, który w naszym teście musimy zastąpić mockiem.
Jednym ze sposobów jest ręczna zmiana zależności w kodzie testu:



Sposób ten ma jednak kilka wad:

- Dla każdego testu (metody testującej) musimy sami tworzyć i przypisywać mocka

- Serwis musi dostarczać settera dla zależności mockowanej, podczas gdy w przypadku wstrzykiwania zależności za pomocą adnotacji setter nie jest wymagany

- Mockujemy tylko konkretną zależność między dwoma beanami. Jeżeli serwis który mockujemy jest używany przez inny serwis biorący udział w teście (np. DoSthService używa HttpClient i OtherService, a OtherService używa HttpClient) , musimy mockować każdą zależność osobno

Lepszym rozwiązaniem jest podmiana serwisu HttpClient bezpośrednio w kontekście aplikacji. Oczywiście nie możemy zmienić pliku applicationContext.xml, musimy stworzyć odrębny kontekst aplikacji i wskazać go w konfiguracji naszego testu:



No dobrze, ale jak stworzyć mocka w pliku konfiguracji kontekstu aplikacji, skoro nasz mock nie jest utworzoną przez nas osobną klasą, ale został wygenerowany automatycznie przez framework Mockito ( Mockito.mock(HttpClient.class) ) ?

Rozwiązanie jest proste. Metodę Mockito.mock należy zadeklarować jako metodę fabrykującą naszego mocka podając jako argument tej metody klasę obiektu mockowanego:

Dostrajanie konfiguracji


Kopiowanie całej konfiguracji kontekstu aplikacji w celu nadpisania jednego serwisu prowadzi do redundancji kodu (kodu konfiguracji), a zatem zwiększa koszt utrzymania aplikacji.
Jeżeli chcemy tego uniknąć, możemy zdefiniować dwa pliki konfiguracji kontekstu aplikacji w konfiguracji naszego testu:



W pliku applicationConfig-test.xml definiujemy tylko mocki, które zastąpią istniejące beany w konfiguracji głównej.
Zwróćmy uwagę na kolejność deklaracji plików konfiguracji. Ważne jest aby deklaracja pliku konfiguracji mocków następowała po deklaracji pliku głównego konfiguracji.

Autowstrzykiwanie mocków


W przypadku gdy korzystamy z autowstrzykiwania zależności, nasz mock będzie dostępny tylko gdy wstrzykiwanie zależności odbywa się na podstawie nazwy. A zatem mock nie zostanie znaleziony jeśli używamy adnotacji @Autowired, dla której wstrzykiwanie odbywa się na podstawie typu. Rozwiązaniem może być dodanie kwalifikatora wskazującego nazwę zależności:


Jednak lepszym rozwiązaniem jest zastosowanie adnotacji @javax.annotation.Resource. W tym przypadku zależność najpierw wyszukiwana jest po nazwie, a w przypadku braku pasującej nazwy, po typie.

Włączanie/wyłączanie mocków


Czasami ten sam test integracyjny chcemy uruchamiać zarówno z serwisem w postaci mocka jak i z serwisem rzeczywistym. Jeśli stosujemy opisaną powyżej metodę nadpisywania beanów z konfiguracji głównej mockami z konfiguracji testowej, włączenie/wyłączenie mocka można łatwo osiągnąć modyfikując plik konfiguracji mocków (dodając bądź usuwając mocka). Jednak co z kodem sterującym zachowaniem mocka (patrz kod poniżej)?



Uruchomienie tego kodu na obiekcie nie będącym mockiem zakończy się wyjątkiem. Należy zatem kod sterujący mockiem wykonać warunkowo tylko jeśli obiekt rzeczywiście jest mockiem. Sprawdzenie implementujemy następująco:


Współdzielenie mocka


Jeżeli chcielibyśmy użyć tego samego mocka w kilku metodach testowych musimy pamiętać o zresetowania stanu mocka przed każdym testem.
Najprostszym rozwiązaniem jest dodanie do klasy testu metody resetMocks z adnotacją @org.junit.Before:


Mockowanie częściowe


Na koniec przedstawię w jaki sposób utworzyć w konfiguracji kontekstu aplikacji częściowego mocka, czyli obiekt, którego zachowanie tylko w części chcemy mockować (np. zmieniając tylko rezultat wywołania metody):

W tym przypadku tworzymy mocka przy pomocy metody fabrykującej Mockito.spy, podając jako argument tej metody referencję do istniejącego bean-a.

22 October 2010

ZK - Ajax dla każdego

ZK opiera swoją architekturę na technologii Ajax. Komunikacja klient-serwer z wykorzystaniem Ajax-a w całości realizowana jest przez silnik ZK i jest niewidoczna/przezroczysta z punktu widzenia aplikacji używającej ZK. To framework decyduje kiedy wysłać żądanie do serwera oraz kiedy i jakie elementy strony odświeżyć. Dodając do tego warstwę komponentów i zdarzeń, którą dostarcza ZK, otrzymujemy interfejs programistyczny znany z frameworków desktopowych (np. Swing). Mamy zatem obiekt kontrolera, którego metody wywoływane są w wyniku akcji użytkownika oraz komponenty GUI, którymi kontroler steruje. Kontroler może bezpośrednio wywoływać metody komponentów, bądź modyfikować obiekty modelu, z których komponenty GUI korzystają. Zarówno komponenty GUI jaki i obiekty modelu definiują zdarzenia jakie wysyłają w wyniku akcji użytkownika bądź wykonania określonego kodu.
Tworzenie aplikacji w ZK nie różni się zatem znacząco od tworzenia aplikacji desktopowej. Co więcej, ZK oferuje nowoczesne rozwiązania niedostępne w starszych technologiach takich jak Swing (np. wbudowany data binding, język tworzenia interfejsu użytkownika oparty na xul.

Tyle teorii, zobaczmy jak wygląda kod aplikacji opartej o ZK analizując konkretne przykłady.

Ajax w akcji



W powyższym przykładzie kontroler w metodzie obsługującej zdarzenie onAddPerson tworzy obiekt osoby (obiekt klasy Person) i wypełnia go danymi wprowadzonymi na stronie przez użytkownika. Dane te odczytywane są bezpośrednio z komponentów GUI (textbox, combobox, checkbox). ZK umożliwia wstrzyknięcie komponentów GUI do kontrolera na podstawie zgodności nazwy zmiennej w kontrolerze i atrybutu id komponentu GUI.
Po zapisaniu obiektu osoby do bazy, wywoływana jest metoda addPersonRecord, w której nazwa osoby (String) dodawana jest do listy wyświetlanej na ekranie (komponent personsList klasy Listbox). Rezultatem modyfikacji komponentu Listbox będzie odświeżenie listy na stronie przeglądarki. Dzięki technologii Ajax odświeżony zostanie tylko fragment strony zawierający listę osób.
Jak widać, mechanizm odświeżania działa automatycznie i nie wymaga żadnej ingerencji ze strony obiektu kontrolera.

Inteligentny model


Komponenty GUI przeznaczone do wyświetlania/edycji danych (listbox, combobox, grid, tree) mogą być powiązane z zewnętrznym obiektem modelu przechowującym dane. Rezultatem użycia modelu jest odseparowanie logiki związanej z tworzeniem/modyfikacją danych od logiki ich wyświetlania (zgodnie z wzorcem MVC). Dla komponentu Listbox przewidziano interfejs ListModel i dostarczono wiele implementacji tego interfejsu przeznaczonych do obsługi różnych typów kolekcji np. ListModelArray, ListModelList, ListModelSet, ListModelMap. Po przypisaniu modelu do listy, kontroler nie musi odwoływać się bezpośrednio do komponentu listy. Wszelkie operacje (dodanie, usunięcie elementu, zmiana selekcji) dokonuje na obiekcie modelu:


Model w momencie modyfikacji (dodanie/usunięcie elementu) generuje zdarzenie, w którym zawarta jest szczegółowa informacja o zaistniałej zmianie. Komponent listy posiada obiekt nasłuchujący na zdarzenia pochodzące z modelu, dzięki czemu jest informowany o tym kiedy i w jakim zakresie musi dokonać odświeżenia swojego stanu. Modyfikacje w warstwie modelu są zatem automatycznie odzwierciedlane na stronie (w przeglądarce).

Widok


Logiką wyświetlenia danych dostarczonych przez model zajmuje się renderer. W naszym przykładzie metoda render, którą musi zaimplementować renderer listy wygląda następująco:

Tworzenie własnego renderera zwykle nie jest jednak konieczne. Prostszą i efektywniejszą metodą jest użycie mechanizmów bindowania w kodzie strony:

Stosując data binding w wygodny sposób tworzymy powiązanie między komponentami GUI i danymi z modelu, które te komponenty mają wyświetlać.
W podanym przykładzie lista nie jest skomplikowana, wyświetla bowiem tylko nazwę osoby. Jednak stworzenie bardziej skomplikowanej listy (np. dodanie większej ilości kolumn, zagnieżdżenie w wierszu innych komponentów) nie stanowi najmniejszego problemu. Przykład poniżej:

MVC bez limitów


Jak już wspomniałem, przedstawiony powyżej przykład to realizacja dobrze znanego wszystkim programistom wzorca MVC (Model-View-Controller).
Dzięki jego zastosowaniu kontroler odwołuje się tylko do modelu, a stan modelu odzwierciedlany jest po stronie widoku.
Zauważmy, że widok może odzwierciedlać różnego rodzaju informacje biznesowe, nie tylko dane przeznaczone do wyświetlenia w tabeli, liście czy drzewie. Na przykład może to być informacja o uprawnieniach zalogowanego użytkownika. W zależności czy użytkownik posiada określone uprawnienia czy nie, stan aktywności poszczególnych kontrolek (np. przycisków akcji) może być inny. Co więcej na stan aktywności niektórych kontrolek może wpływać więcej niż jedna informacja.

Jak zrealizować zatem taką funkcjonalność nie łamiąc zasad MVC czyli nie manipulując właściwościami komponentów GUI z poziomu kontrolera?
Jak się okazuje, technologia data binding dostępna w ZK w zupełności wystarcza do realizacji tego typu wymagań.

Załóżmy zatem, że wymaganiem jest aby przycisk Delete znajdujący się pod listą był aktywny tylko jeśli użytkownik posiada odpowiednie uprawnienia oraz został wybrany element na liście.
W modelu przechowywać będziemy obiekt klasy Person, który reprezentuje aktualnie wybraną z listy osobę oraz obiekt klasy AccessMode reprezentujący
tryb dostępu do danych (VIEW / EDIT) dla bieżącej strony dla aktualnie zalogowanego użytkownika.

Dane modelu będziemy przechowywać w kontrolerze (alternatywnie moglibyśmy umieścić je w jakiejś oddzielnej klasie modelu, do której nasz widok miałby dostęp).
Mamy zatem następujące pola w kontrolerze:

Tworzymy, metodę która zwróci nam informację o tym czy akcja Delete jest dostępna dla zalogowanego użytkownika:

W następujący sposób tworzymy przycisk Delete, którego stan (enabled/disabled) chcemy powiązać z danymi znajdującymi się w modelu:

Atrybutem load-after specyfikujemy kiedy komponent powinien odświeżyć swój stan. W naszym przypadku odświeżenie musi nastąpić po każdorazowej zmianie selekcji na komponencie listy.
Zwracam uwagę, iż atrybut load-after jest konieczny gdyż, ani model ani kontroler nie informują widoku o zmianie danych.
Jest to wygodne rozwiązanie, widok aktualizuje się automatycznie w momencie wystąpienia określonego zdarzenia (Więcej o zdarzeniach w ZK napisałem w artykule ZK - tworzenie aplikacji sterowanej zdarzeniami).

Podsumowanie

Chciałbyś napisać aplikację internetową z użyciem Ajax-a i nie wiesz jak się za to zabrać? Dotychczas tworzyłeś aplikacje desktopowe i nie chcesz uczyć się wszystkiego od początku?. Framework ZK będzie dla Ciebie idealnym rozwiązaniem. Tak reklamuje się ZK i jak pokazałem w powyższym artykule, nie ma w tym wiele przesady.


Dowiedz się więcej:

4 October 2010

ZK - tworzenie aplikacji sterowanej zdarzeniami

Większość frameworków/bibliotek odpowiedzialnych za obsługę interfejsu użytkownika definiuje zdarzenia i dostarcza szereg mechanizmów służących do przechwytywania i przetwarzania zdarzeń.
Komponenty GUI oferowane przez te biblioteki specyfikują jakie zdarzenia i kiedy są przez nie generowane oraz na jakie zdarzenia i w jakim celu nasłuchują. Zdarzenia to nie tylko akcje wykonane przez użytkownika takie jak kliknięcie myszką na przycisk, wybór elementu na liście czy naciśnięcie klawisza Enter w polu tekstowym. Komponenty używają bowiem zdarzeń również do komunikacji między sobą dzięki czemu powiązania między komponentami są luźne. Jest to jeden z ważnych czynników zwiększających elastyczność architektury systemu.

Dlaczego zatem nie wprowadzić zdarzeń do warstwy logiki biznesowej? Nie jest to nowa koncepcja, aczkolwiek rzadko spotykana w typowych aplikacjach biznesowych. Wynika to m.in. z braku odpowiedniego wsparcia ze strony frameworków aplikacyjnych. Z nadejściem JEE 6 sytuacja może się zmienić. Specyfikacja CDI definiuje prosty w użyciu model generowania i odbierania zdarzeń (podobny model od dawna oferuje Seam). Model ten jest uniwersalny i może mieć zastosowanie w różnych obszarach/warstwach aplikacji.
W poniższym artykule zaprezentuję rozwiązanie jakie w tym zakresie oferuje framework ZK.

Przykładowa aplikacja


W typowej aplikacji biznesowej mamy zwykle do zaimplementowania obsługę grupy operacji określanej skrótem CRUD (tworzenie, zmiana, usuwanie) wykonywanych przez użytkownika na różnych obiektach biznesowych. W naszej przykładowej aplikacji zamodelujmy obsługę ról użytkowników. Mamy zatem ekran z listą ról oraz ekran edycji/tworzenia roli wyświetlany w oknie dialogowym. Rysunki poniżej.

Zdarzenie biznesowe


Zdarzenie w ZK definiowane jest jako obiekt klasy Event posiadający nazwę (name). Dodatkowo zdarzenie może mieć przypisany komponent docelowy (target) oraz dowolny obiekt z danymi (data).

Zdefiniujmy zatem zdarzenia biznesowe, które chcielibyśmy obsłużyć w naszej aplikacji:

- onEdit - użytkownik wykonał akcję mającą na celu przejście do trybu edycji wybranego obiektu biznesowego

- onAdd - użytkownik wykonał akcję mającą na celu przejście do trybu edycji nowego obiektu biznesowego

- onDelete - użytkownik wykonał akcję mającą na celu usunięcie wybranego obiektu biznesowego

- onSave - użytkownik wykonał akcję mającą na celu zapisanie zmian (modyfikacja bądź utworzenie obiektu biznesowego)

Przypisanie zdarzenia biznesowego do akcji użytkownika


W naszej aplikacji edycja następuje po kliknięciu na element listy, a tworzenie elementu po naciśnięciu przycisku znajdującego się pod listą. W obu przypadkach nastąpi wyświetlenie okna edycji (popup).

Zwróćmy uwagę na atrybuty forward zdefiniowane dla linku (element a w wierszu listy) i przycisku pod listą (element button).
W atrybucie forward podajemy nazwę zdarzenia jakie zostanie propagowane w górę drzewa komponentów. Propagowanie odbywa się poprzez utworzenie nowego zdarzenia (o nazwie podanej po znaku równości) zawierającego zdarzenie źródłowe (o nazwie podanej przed znakiem równości). W naszym przypadku zdarzeniem źródłowym jest zdarzenie systemowe onClick (kliknięcie na przycisk/link). W przypadku nie podania zdarzenia źródłowego, przyjmowane jest domyślne, różne w zależności od komponentu dla jakiego specyfikujemy atrybut forward.
W przypadku linka i przycisku jest to zdarzenie onClick. Zatem oba zapisy są tożsame:

Zdefiniowaliśmy zatem, po jakiej akcji użytkownika nastąpi określone zdarzenie biznesowe.
Odseparowaliśmy tym samym logikę interfejsu użytkownika od logiki biznesowej. Gdybyśmy dla innego obiektu biznesowego edycję chcieli przeprowadzić nie w osobnym oknie, lecz w tym samym, w którym znajduje się lista wyboru, zdarzenie onEdit moglibyśmy zdefiniować jako następstwo wyboru elementu na liście:

W obu przypadkach logika obsługi zdarzenia onEdit będzie taka sama.
Przyjrzyjmy się zatem jak obsłużyć zdarzenie w kodzie aplikacji.

Obsługa zdarzenia


Zdarzenie propagowane jest aż do komponentu okna (window) i następnie zostaje przekazane do kontrolera podpiętego pod to okno (kontroler definiujemy atrybutem apply). W naszym przypadku kontroler jest bean-em zarządzanym przez Springa o nazwie "roleListCtrl" (użycie Spring-a jest opcjonalne).

Kontroler musi zdefiniować metodę odpowiadającą nazwie zdarzenia:
public void onEdit(Event event);


Wiemy zatem, w jaki sposób nasze zdarzenie biznesowe może być utworzone i obsłużone w wyniku akcji podjętej przez użytkownika.
Spyta ktoś, czym to rozwiązanie różni się od klasycznego podejścia używanego w innych frameworkach np. Seam, gdzie możemy analogicznie wywołać metodę w kontrolerze:

Rozpatrzmy różnice między oboma podejściami z punktu widzenia interfejsu wywołania/obsługi komunikatu :

Seam: w metodzie możemy przekazywać dowolną ilość argumentów, dowolnego typu.

ZK: W zdarzeniu możemy przekazać jeden obiekt danych (możemy np. przekazać wybraną rolę: forward=onEdit(role)) . Typem danych jest zawsze Object. Porównując zatem z wywołaniem metody, mamy ograniczoną ilość argumentów i konieczność rzutowania typu po stronie kontrolera. Zwykle jednak nie ma konieczności przekazywania większej liczby argumentów (w naszym przykładzie nie musimy przekazywać roli w zdarzeniu, gdyż framework potrafi wstrzyknąć do kontrolera wybraną rolę automatycznie).
Podkreślić należy, że oba problemy wynikają z ograniczeń jakie niesie ze sobą propagowanie zdarzeń przy przy pomocy atrybutu forward. Nie istnieje tutaj możliwość stworzenia własnej klasy zdarzenia. Gdy wysyłamy zdarzenie w kodzie aplikacji (przykład później),problemy powyższe nie istnieją.

Przejdźmy w końcu do przykładów, gdzie zdarzenia zaczynają pokazywać swoją moc:)

Zdarzenia w akcji


W przypadku gdy wyświetlamy kilka okien na stronie możemy używać zdarzeń do komunikacji między oknami sterując w ten sposób logiką aplikacji.

Dodajmy na naszej stronie z listą ról nowy panel z listą użytkowników. Lista ról niech wyświetla tylko role dla wybranego użytkownika (z możliwością ich edycji). Obie listy umieszczamy w oddzielnych oknach (komponentach window) dzięki czemu obie listy możemy obsługiwać oddzielnymi kontrolerami (w obu oknach chcemy obsłużyć logikę CRUD).
Odświeżenie listy ról po wybraniu użytkownika obsługujemy następująco:




Dzięki temu, że zdefiniowaliśmy zdarzenie onUserSelected, możemy zarejestrować słuchaczy obserwujących to zdarzenia w celu zaimplementowania dodatkowej logiki. Jako przykład wykorzystam wbudowany w ZK mechanizm sterowania bindowaniem danych. Co to jest bindowanie? Jest to możliwość bezpośredniego połączenia warstwy modelu (danych) z kontrolkami wyświetlającymi/modyfikującymi te dane. W ZK bindowanie może obejmować dowolne właściwości kontrolek np. stan kontrolki (czy kontrolka jest aktywna czy nie (enabled/disabled)).
Dodajmy zatem wymaganie w naszej aplikacji aby usunięcie roli było możliwe tylko dla użytkownika nowo utworzonego (dla którego pole registered = false). Musimy zatem przy zmianie selekcji użytkownika deaktywować bądź aktywować przycisk Delete pod listą ról.
Korzystając z możliwości mechanizmu bindowania ZK konfigurujemy właściwość disabled dla przycisku Delete:

Użycie @ w wyrażeniu oznacza zastosowanie bindingu. W naszym przypadku odczytujemy pole registered z obiektu użytkownika przekazanego uprzednio do kontrolera (patrz kod wyżej). Za pomocą parametru load-after, wskazujemy zdarzenie, po nastąpieniu którego nastąpi odświeżenie przycisku (odczytanie danych z modelu). Framework automatycznie zarejestruje odpowiedniego listenera w komponencie naszego okna nasłuchującego na zdarzenie onUserSelected.

Zauważmy co zyskujemy. Kod obsługujący zdarzenie onUserSelected jest czysty. Modyfikujemy w nim tylko stan modelu, czyli ustawiamy zmienną selectedUser i odświeżamy listę ról (model). Zarówno przy odświeżaniu listy ról jak i odświeżaniu przycisku na stronie zostanie odczytane pole selectedUser. Kontroler nie musi wiedzieć kiedy odświeżyć przycisk, nie musi nawet wiedzieć o jego istnieniu.

Zdarzenia globalne


Przedstawione dotychczas zdarzenia były wysyłane do konkretnego komponentu. Nie zawsze jest to pożądane. Jedną z podstawowych cech architektur sterowanych zdarzeniami jest możliwość niezależnego wysyłania i odbierania zdarzeń. Emitent zdarzenia nie musi znać odbiorców. Odbiorca nie musi wiedzieć skąd zdarzenie pochodzi. Jak tę funkcjonalność zrealizować w ZK pokaże znowu na konkretnym przykładzie.
Dodajmy na górze naszej strony panel zawierający nazwę zalogowanego użytkownika. Pojawia się kwestia odświeżenia zawartości panelu w momencie modyfikacji nazwy aktualnie zalogowanego użytkownika. W kontrolerze obsługującym okno edycji użytkownika implementujemy obsługę zdarzenia onSave.

Wysyłamy zatem zdarzenie onUserChanged bez adresata do kolejki o zdefiniowanej przez nas nazwie "QUEUE_GLOBAL".
Zauważmy, że tym razem zdefiniowaliśmy własną klasę zdarzenia (UserChangedEvent), w której przekazujemy obiekt użytkownika. Standardowo, rejestracja w kontrolerze słuchacza zdarzeń z tej kolejki wygląda w następujący sposób:

Rejestrację można uprościć i jednocześnie ułatwić kontrolerowi obsługę zdarzenia tworząc klasę pośrednicząca w odbieraniu i przekazywaniu zdarzeń do wybranego przez nas komponentu (nazwijmy ją EventsManager):

Teraz w kontrolerze nagłówka możemy w sposób standardowy zaimplementować obsługę zdarzenia onUserChanged.


Używając zdarzeń globalnych uwalniamy emitenta zdarzenia od konieczności znajdowania komponentu docelowego. Jednocześnie uwalniamy odbiorcę od rejestracji nasłuchiwania w konkretnym komponencie. Wystarczy, że obie strony uzgodnią kanał (kolejkę) komunikacji.
Zdarzenia globalne domyślnie wysyłane i odbierane są w kontekście strony przeglądarki (desktop-level).
ZK pozwala również funkcjonować zdarzeniom w kontekście całej aplikacji. Dzięki temu strona może zostać odświeżona pomimo braku akcji ze strony zalogowanego użytkownika (np. na skutek operacji wykonanej przez scheduler-a uruchomionego na serwerze). Umożliwia to technologia Push Server, oferowana wewnątrz ZK.



Dowiedz się więcej:

Dostrajanie warstwy ORM w projekcie wielomodułowym

Częstym jak sądzę przypadkiem w średnich i większych projektach informatycznych jest współdzielenie modelu domeny przez kilka niezależnych aplikacji.
Takimi aplikacjami mogą być np.: web portal dla klientów, wewnętrzna aplikacja administracyjna, moduł raportujący.
Wspólne dane, z których korzystają aplikacje, nie są wcale powodem do tworzenia wspólnego modelu domeny. Polecam na ten temat prezentację DDD - putting model to work), której którkie podsumowanie można znaleźć tutaj: IT-Researches Blog.
Zakładając jednak, że mamy jeden model (co jest częstą praktyką) pojawia się kwestia współdzielenia modelu ORM zdefiniowanego
jako mapowania obiektów do tabel w bazie relacyjnej.
Jak się bowiem często okazuje wymagania poszczególnych aplikacji w tym zakresie są różne.
Dotyczyć to może takich kwestii jak sposób inicjalizacji pól encji (lazy vs egear fetching).
Zagadnienie, jakie dokładnie ustawienia ORM warto dostrajać i kiedy, odłożę na później.
W tym wpisie chciałbym przedstawić w jaki sposób skonfigurować projekt aby umożliwić poszczególnym aplikacjom dostosowanie warstwy ORM do ich potrzeb oraz jakie problemy
przy tworzeniu takiej konfiguracji napotkałem.

Konfiguracja projektu


Wykorzystywane technologie:
- Maven
- Spring
- Hibernate

Mamy zatem projekt wielomodułowy, w skład którego wchodzą poszczególne aplikacje oraz następujące moduły współdzielone:

- model domeny - (encje/domain objects)
- dao - konfiguracja dostępu do bazy danych, klasy dao

Moduł - model domeny

Model domeny stanowią encje (obiekty POJO) opisane adnotacjami Hiberanate Annotations.
Adnotacje są dobrym sposobem na zdefiniowanie domyślnych mapowań ORM. Poszczególne aplikacje mają bowiem możliwość nadpisania domyślnych mapowań przy użyciu plików konfiguracyjnych xml (hbm.xml). Zwracam uwagę na to, że Hibernate Annotations bazują na specyfikacji JPA jednak nie wymagają użycia modułu JPA (dostarczającego interfejs javax.persistence.EntityManager).

Moduł - dao

Konfigurację SessionFactory tworzymy wykorzystując Spring-ową fabrykę wspierającą Hibernate Annotations.



W parametrze annotatedClasses podajemy listę naszych encji. Co warte uwagi Spring umożliwia wskazanie pakietu który będzie automatycznie skanowany w poszukiwaniu encji (parametr packagesToScan).

Nas jednak bardziej interesuje parametr mappingDirectoryLocations. Wskażemy w nim katalog, z którego załadowane zostaną pliki hbm.xml.
W ten sposób umożliwiamy aplikacjom dostarczenie własnych mapowań ORM.

Przykład


Uporawszy się z konfiguracją, przetestujmy jak działa nadpisywanie mapowań na konkretnym przykładzie.

Mamy zatem klasę FeedCategory, która dziedziczy po BaseEntity i zawiera listę podkategorii (pole subCategories).



Jak widzimy, domyślnie Hibernate załaduje listę podkategorii leniwie (w momencie użycia) co zostało zdefiniowane ustawieniem fetch = FetchType.LAZY.
Załóżmy jednak, że chcemy aby w naszej aplikacji podkategorie były ładowane "chciwie" (ang. eagerly) a więc zaraz po załadowaniu obiektu głównego.

W tym celu tworzymy w module konkretnej aplikacji katalog orm/custom-mappings, który wskazaliśmy w konfiguracji SessionFactory (w projekcie maven-owym umieszczamy ten katalog w gałęzi src/main/resources) i umieszczamy w nim plik feedCategory.hbm.xml:



Tym razem ustawienie sposobu pobierania listy kategorii definiujemy atrybutem lazy="false" (czyli chciwie).

Problem


Napotykamy problem, który wydawało się nie powinien zaistnieć. Mianowicie adnotacja @MappedSuperclass nie ma odpowiednika w konfiguracji mapowań Hibernate.

Obejściem tego problemu jest zdefiniowanie pól z klasy BaseEntity w pliku mapowań klasy FeedCategory. Jednak jest to niewygodne. Wyobraźmy sobie bowiem, że nadpisujemy 10 klas po czym dokonujemy zmiany w domyślnej konfiguracji BaseEntity... Będziemy musieli tę zmianę wprowadzić również w 10 plikach hbm.xml..
Drugim problemem (który być może wynika z pierwszego - temat nie do końca sprawdzony) jest konieczność zdefiniowania wszystkich pól klasy FeedCategory.
Nie można zatem nadpisać tylko zmienionego elementu konfiguracji, trzeba zdefiniować całe mapowanie na nowo.

Rozwiązanie


Rozwiązaniem powyższych niedogodności jest skonfigurowanie Hibernate jako dostawcy JPA i zastąpienie mapowań w formacie hbm.xml mapowaniami xml w standarcie JPA.

W tym celu konfigurację SessionFactory zastępujemy konfiguracją EntityManagerFactory ponownie korzystając z udogodnień jakie oferuje Spring, tym razem dla JPA:



Szczegółowe ustawienia dostarczamy w pliku persistence.xml, w którym również specyfikujemy listę naszych encji
(ustawiając parametr hibernate.archive.autodetection nakazujemy Hibernate Entity Manager aby wyszukał encje w określonych lokalizacjach,
więcej informacji na ten temat tutaj: Do I need class elements in persistence.xml):


Pozostaje skonfigurować wykrywanie mapowań xml dostarczonych przez poszczególne aplikacje.
Niestety w przypadku JPA nie mamy analogicznego do mappingDirectoryLocations parametru zarówno na poziomie konfiguracji w pliku persistence.xml jak i udogodnień Spring-a.
Rozwiązaniem jest przekazanie do LocalContainerEntityManagerFactoryBean klasy implementującej interfejs
PersistenceUnitPostProcessor. Postprocesor ma możliwość modyfikowanie opcji konfiguracyjnych, w tym dodanie mapowań xml.



Możemy zatem w aplikacji nadpisać mapowania domyślne tworząc plik orm.xml (jest to standardowa nazwa pliku określona w specyfikacji JPA, aczkolwiek plików z mapowaniami może być wiele).
W naszym przykładzie plik orm.xml wygląda następująco:



Jak widać, ostatecznie udało się osiągnąć cel czyli nadpisać tylko to co wymagało dostosowania.
Niestety wymagało to zmiany konfiguracji projektu w celu integracji standardu JPA.