Odwzorowanie związków między obiektami jest jednym z najważniejszych zadań
każdego narzędzia ORM (Object / Relational Mapping). Możliwości zarządzania
asocjacjami w Hibernacie jest bardzo dużo, a poznanie ich wszystkich wymagałoby zapewne wielu
dni intensywnej pracy. Dlatego też ograniczymy się tu do najczęściej napotykanych,
standardowych problemów oraz ich wzorcowych rozwiązań.
Zanim jednak przejdziemy do przykładów, musimy wyjaśnić kwestię zarządzania asocjacjami
(managing associations). Jeśli ktoś pracował z CMP 2.0/2.1, pamięta zapewne, że tam związki
działały automatycznie w obie strony, tzn. jeśli wywołaliśmy metodę item.setOwner(owner)
,
to kontener automatycznie wywoływał metodę owner.getItems().add(item)
.
Uwaga: Hibernate NIGDY nie działa w ten sposób. Dla niego asocjacje są zawsze jednostronne,
czyli związek pomiędzy właścicielem a przedmiotem jest czymś niezależnym od związku między
przedmiotem a właścicielem.
Podejście to ma co najmniej dwa, solidne, uzasadnienia. Po pierwsze, na poziomie Javy asocjacje są zawsze jednostronne -
a zadaniem Hibernate'a jest implementacja trwałości tzw. POJOs (Plain Old Java Objects). Po drugie, o obiektach hibernatowych,
w przeciwieństwie do entity beanów, nie zakłada się, że są zarządzane przez kontener.
public class Krowa { private Stado stado; ... public void setStado(Stado stado){ this.stado = stado; } public Stado getStado(){ return stado; } ... }Zaś do pliku
krowa.hbm.xml
dodalibyśmy fragment jak poniżej: <class name="Krowa" table="KROWA"> ... <many-to-one name="stado" column="STADO_ID" class="Stado" not-null="true" /> </class>Kolumna
STADO_ID
stanie się kluczem obcym w tabeli KROWA
do tabeli STADO
.
Tutaj podaliśmy explicite nazwę klasy (Stado), jednak w ogólności nie jest to konieczne - Hibernate
jest w stanie sam odgadnąć tą klasę - stosując metodę refleksji (reflection - ang.), czyli
analizując kod klasy Krowa. Na razie mamy związek jedynie w jedna stronę. W praktyce czesciej bedziemy jednak chcieli korzystać ze związków dwustronnych, czyli również w obiekcie klasy Stado pamiętać Krowy, które do danego stada należą. Do standardowego kodu tej klasy dopisalibyśmy następujące linie:
public class Stado { ... private Set krowy = new HashSet(); public void setKrowy (Set krowy){ this.krowy = krowy; } public Set getKrowy (){ return krowy; } public void dodajKrowe(Krowa krowa){ krowa.setStado(this); krowy.add(krowa); } ... }
Kod pliku mapującego Stado zawierałby linie:
<class name="Stado" table="STADO"> ... <set name="krowy" inverse="true" cascade="save-update"> <key-column="STADO_ID" /> <one-to-many class="Krowa" /> </set> </class>Wyjaśnienia wymagają dwa atrybuty elementu set: inverse oraz cascade.
krowa.setStado(Stado)
zmieni pole STADO_ID
w tabeli KROWA
,
natomiast wywołanie stado.getKrowy.add(krowa)
nie spowoduje żadnej czynności na poziomie bazy danych. session.save(krowa)
.
Tradycyjnie, działa to tylko w jedną stronę. Gdybyśmy chcieli, aby nowe stado synchronizowało
się z bazą gdy tylko ustawimy je jako stado w jakims obiekcie klasy Krowa w stanie 'persistent' - należałoby
dodać atrybut cascade="save-update" po stronie <many-to-one>, w pliku krowa.hbm.xml.
Gdyby ustawić cascade na "all-delete-orphan", byłoby to jeszcze mocniejsze: oprócz
dotychczasowej zależności, krowa byłaby usuwana z bazy automatycznie przy usuwaniu jej stada.
Jest jeszcze kilka możliwych wartości tego atrybutu, ale tak szczegółowa analiza wykracza
poza ramy tej prezentacji. <many-to-one name="adresId" class="Adres" column="AdresId" cascade="all" unique="true" />Natomiast do klasy Adres dodajemy pole np. mieszkaniec typu Czlowiek oraz do pliku adres.hbm.xml:
<one-to-one name="czlowiek" class="Czlowiek" property-ref="mieszkaniec" />Ta implementacja ma następującą wadę: nie da się stworzyć dwóch dwustronnych związków jeden do jednego między danymi klasami, np Czlowiek nie może mieć adresu stałego i tymczasowego tak, by te adresy miały odnośnik do człowieka. Okazuje się, że w Hibernate coś takiego jest niewykonalne. Można jedynie zrobić takie związki jednostronnymi (co często wystarcza), ale autorzy projektu odradzają tworzenie więcej niż jednego związku 1-1 pomiędzy dwoma klasami.
Plik Czlowiek.hbm.xml: <one-to-one name="adres" class="Adres" cascade="save-update" /> Plik Adres.hbm.xml: <class name="Adres" table="Adres"> <id name="id" column="ADRES_ID"> <generator class="foreign"> <param name="property">czlowiek</param> </generator> </id> ... <one-to-one name="czlowiek" class="Czlowiek" constrained="true"/> </class>Co konkretnie napisaliśmy? Utworzenie generatora id adresu jako foreign mówi, że Hibernate będzie go szukał w rekordzie w innej tabeli, odpowiadającym atrybutowi czlowiek (klasy Czlowiek). Atrybut constrained mówi tyle: klucz główny klasy Adres jest jednocześnie kluczem obcym i odnosi się do klucza głównego Czlowieka.