4 October 2010

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.

2 comments:

  1. Bardzo fajny wpis - nie wiedziałem, że można tak nadpisywać konfiguracje.
    Ale czy nie było by prościej dla rożnych typów klientów (nie tylko zresztą dla typów klientów, czy nawet tylko różnych use cases) używać po prostu różnych finderów HQL-owych?
    Wtedy możemy dowolnie definiować, jak obiekty jak są ładowane (eager vs lazy).
    Pod względem pracy i konfiguracji wydaje się to chyba trochę prostsze?

    ReplyDelete
  2. >>Ale czy nie było by prościej dla rożnych typów klientów (nie tylko zresztą dla typów klientów, czy nawet tylko różnych use cases) używać po prostu różnych finderów HQL-owych?

    Należy rozróżnić ustawienie statyczne fetchingu od ustawienia dynamicznego. W tym artykule opisałem dostrajanie fetchingu statycznego (definiowanego w mapowaniach orm).

    Fetching statyczny zawsze można nadpisać w konkretnym zapytaniu hql (fetching dynamiczny). Nie zawsze jest to wygodniejszy sposób (jeżeli nadpisujemy to samo mappowanie w wielu zapytaniach). W przypadku kiedy klasa A definiuje kolekcję obiektów również klasy A to zaimplementowanie eager-fetchingu w sposób dynamiczny wymaga dodatkowej pracy.

    ReplyDelete