Asocjacje


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.

  1. Jeden do wielu (one-to-many)
    Związek jeden do wielu jest najważniejszym rodzajem asocjacji, i jedynym niezbędnym. Każdy bowiem związek wiele do wielu można rozbić na dwa jeden-do-wielu, natomiast asocjacji 1-1 można nie używać w ogóle. Najprostszy związek wiele-do-jednego (będziemy tego używać zamiennie z jeden-do-wielu): wiele krów może należeć do jednego stada.
    Kod w Javie, opisujący klasę Krowa, rozszerzyłby się w następujący sposób:
       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);
       }
       ...
    }
  2. 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.




  3. Wiele do wielu (many-to-many)

    Związki wiele do wielu w relacyjnych bazach danych nie występują. Mogą jednak występować w językach obiektowych i można je obsługiwać również Hiberate'em. Jest to jednak trochę bardziej złożone niż związki jeden-jeden lub jeden-wiele. W tej prezentacji nie będziemy próbowali ich opisać. Głównie dlatego, że autorzy Hibernate'a zdecydowanie odradzają korzystania z tego mechanizmu. Zawsze można bowiem wprowadzić klasę pośrednią i dwa związki jeden do wielu, co ma praktycznie same zalety (przede wszystkim można potem do tej klasy dodawać nowe atrybuty!). Jeśli jednak ktoś się uprze, że musi zrobić czyste wiele do wielu - jest taka możliwość, należy wgłębić się w dokumentację projektu.



  4. Jeden do jednego (one-to-one)
    Za pomoca kluczu obcego
    Najprostszym sposobem na zamodelowanie tej zależności jest wykorzystanie związku... jeden-do-wielu. Wystarczy bowiem po stronie wielu dodać warunek: klucz obcy musi być unikalny i... już. Kluczowe fragmenty plików .hbm.xml dla przykładowych klas Człowiek i Adres:
    Do klasy Czlowiek dodajemy atrybut adres typu Adres, zaś do pliku czlowiek.hbm.xml dopisujemy:
    <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.
    Za pomoca kluczu głównego
    W tym podejściu odpowiadające sobie rekordy w dwóch tabelach mają mieć takie same klucze główne. Wykluczamy tu już na wstępie możliwość zdefiniowania dwóch związków jeden do jednego pomiędzy dwoma obiektami (nie tylko obustronnych związków, ale jakichkolwiek!). Pliki .hbm.xml dla przykladu z człowiekiem i jego adresem (wyjaśnienie poniżej):
    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.