Dostrajanie warstwy ORM w projekcie wielomodułowym
Paweł Kaczor - 7 września 2010
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:
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.
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
p:dataSource-ref="dataSource">
<property name="annotatedClasses">
<list>
<value>com.example.domain.FeedCategory</value>
[...]
</list>
</property>
<property name="mappingDirectoryLocations">
<list>
<value>classpath:orm/custom-mappings/</value>
</list>
</property>
[...]
</bean>
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
).
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATED_DATE")
private Date createdDate;
[...]
}
@Entity
public class FeedCategory extends BaseEntity {
[...]
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PARENT_ID")
private FeedCategory parent;
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<FeedCategory> subCategories = new ArrayList<FeedCategory>();
[...]
}
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:
<hibernate-mapping package="com.example">
<class name="FeedCategory">
<id name="id" />
<property name="createdDate" column="CREATED_DATE" type="date"/>
[...]
<bag name="subCategories" inverse="true" lazy="false">
<key column="PARENT_ID" />
<one-to-many entity-name="com.example.FeedCategory"/>
</bag>
</class>
</hibernate-mapping>
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:
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
p:persistence-xml-location="classpath:META-INF/persistence.xml" p:data-source-ref="dataSource">
[...]
<property name="persistenceUnitPostProcessors">
<list>
<bean class="com.example.spring.jpa.DefaultPostprocessor" />
</list>
</property>
</bean>
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):
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" [...]>
<persistence-unit>
<class>com.example.domain.FeedCategory</class>
[...]
</persistence-unit>
</persistence>
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.
public class DefaultPostprocessor implements PersistenceUnitPostProcessor, ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) {
Resource resource = resourceLoader.getResource("classpath:orm.xml");
if (resource.exists()) {
pui.addMappingFileName("orm.xml");
}
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
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:
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" [...]>
<entity class="com.example.domain.FeedCategory">
<attributes>
<one-to-many name="subCategories" target-entity="com.example.domain.FeedCategory" mapped-by="parent" fetch="LAZY"/>
</attributes>
</entity>
</entity-mappings>
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.
http://pkaczor.blogspot.com/2010/10/dostrajanie-warstwy-orm-w-projekcie.html
Paweł Kaczor
Programista, pasjonat.
Twórca Akka-DDD – frameworku do budowy skalowalnych systemów w architekturze DDD/CQRS/ES. Interesuje się programowaniem funkcyjnym. W wolnym czasie kraulista, szachista amator.
@PavelKaczor
pkaczor.blogspot.com
github.com/pawelkaczor
Comments