W niniejszym artykule chciałbym zaprezentować mapowanie relacji jeden-do-wielu i bliźniaczą do niej relację wiele-do-jednego. W tego typie relacji możemy rozróżnić byty na jeden byt nadrzędny - po jednej stronie relacji i dowolną ilość bytów podrzędnych - po drugiej stronie relacji.
Relacja ta jest o tyle szczególna, że w świecie relacyjnym jej implementacja jest punktem wyjściowym do implementowania relacji jeden-do-jednego i wiele-do-wielu.
Rozróżniamy następujące warianty relacji tego typu:
- dwukierunkową relacje jeden-do-wielu/wiele-do-jednego: obiekt nadrzędny posiada kolekcję referencji do obiektów podrzędnych, które posiadają referencję do obiektu nadrzędnego;
- jednokierunkowa relacja jeden-do-wielu: obiekt nadrzędny posiada kolekcję referencji do obiektów podrzędnych;
- jednokierunkowa relacja wiele-do-jednego: obiekt podrzędny posiada referencję do obiektu nadrzędnego;
1. Przykład - dziedzina problemu
Tradycyjnie w celach pomocniczych posłużę się mało skomplikowanym przykładem. A oto jego model obiektowy:
Jest to układ dwóch klas
Osoba
oraz
Kontakt
. Ta ostatnia reprezentuje dane kontaktowe osoby takie jak nr telefonu, czy też adres email.
Osoba
agreguje całkowicie
Kontatk
'y (wiązanie typu kompozycja), co dodatkowo uwidocznia nadrzędność jednego bytu do drugiego. Oczywiście omawiana relacja dotyczy się również wiązań typu asocjacja jak i zwykłej agregacji, jednak wybrałem kompozycję, gdyż w modelu relacyjnym będzie się to wiązało z dodatkowym ograniczeniem
not null
.
W relacyjnym świecie przykładowy model będzie prezentował się następująco:
2. Dwukierunkowa relacja wiele-do-jednego/jeden-do-wielu
Po modelu obiektowym, w którym występują połączeniach między obiektami, bardzo często oczekujemy możliwości dwukierunkowej "nawigacji" między powiązanymi obiektami. W naszym przykładzie oznacza to, że oczekujemy możliwości przejścia od obiektu
Osoba
do powiązanych z nią obiektów
Kontakt
i odwrotnie.
Aby sprostać tym wymaganiom musimy odpowiednio mapować obie te klasy.
2.1. Mapowanie klasy Kontakt
Encja
Kontakt
jest właścicielem powiązania, gdyż posiada klucz obcy do encji
Osoba
.
@Entity
@Table(name="kontakt")
public class Kontakt {
...
private Osoba osoba;
@ManyToOne(optional=false)
@JoinColumn(name="oso_id") // domyslnie kolumna by sie nazywala osoba_oso_id
public Osoba getOsoba() {
return osoba;
}
}
JPA wymaga jedynie, żeby w obiekcie podrzędnym oznaczyć referencje do obiektu nadrzędnego za pomocą adnotacji
@ManyToOne
. Ja jednak dodatkowo nadpisałem pewne domyślne ustawiania. I tak:
- ustawiłem atrybut
optional=false
tej adnotacji, co oznacza, że referencja do
Osoba
musi być ustawiona, co zostanie odpowiednio wyrażane podczas automatycznego generowania schematu bazy danych przez dodanie ograniczenia not null;
- poprzez adnotację
@JoinColumn
ustawiłem nazwę kolumny, która jest kluczem obcym do tabeli
Osoba
. Domyślnie by została użyta kolumna o nazwie [nazwa_tabeli_nadrzędnej]_[nazwa_klucza_głównego_tabeli_nadrzędnej];
2.2. Mapowanie klasy Osoba
@Entity
@Table(name="osoba")
public class Osoba {
...
private List<Kontakt> kontakty;
@OneToMany(cascade={CascadeType.PERSIST, CascadeType.REMOVE}, mappedBy="osoba")
@Cascade( {org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
public List<Kontakt> getKontakty() {
return kontakty;
}
public void dodajKontakt(Kontakt kontakt) {
if (null == kontakty) {
kontakty = new ArrayList<Kontakt>();
}
kontakt.setOsoba(this);
kontakty.add(kontakt);
}
}
Dla obiektu nadrzędnego jest wymagane, aby oznaczyć kolekcje do referencji obiektów podrzędnych za pomocą adnotacji
@OneToMany
. Ponieważ jest to wiązanie dwukierunkowe i mapowanie do modelu relacyjnego zostało już określone w klasie
Kontakt
, to należy wyrazić to poprzez atrybut
mappedBy
tej adnotacji, któremu należy przypisać wartość będącą nazwą referencji (w obiekcie podrzędnym) do obietku nadrzędnego. Z atrybutu
mappedBy
należy skorzystać nie tylko, żeby nie musieć podwójnie określać fizycznych parametrów modelu relacyjnego, ale przede wszystkim dlatego, żeby uniknąć zdublowanych zapisów do bazy danych. Atrybut
mappedBy
jest niedostępny dla adnotacji
@ManyToOne
.
Poza tym mapowanie jest wyposażone w parametry określające działania kaskadowe, ale o nich napiszę trochę więcej przy omawianiu działań utrwalania i usuwania obiektów.
2.3. Utrwalanie obiektów w bazie danych
Utrwalania obiektów może być wykonywane w sposób rozdzielny lub kaskadowy.
Schemat
rozdzielnego utrwalania obiektów w naszym przykładzie wyglądałby następująco.
Najpierw utworzenie i zapis obiektu
Osoba
:
Osoba o = new Osoba();
em.persist(o);
Następnie utworzenie, powiązanie z osobą i zapis obiektu
Kontakt
:
Kontakt k = new Kontakt("nr tel.", "+48123124234");
o.dodajKontakt(k);
em.persist(k);
Tu zwrócę uwagę, że metoda
dodajKontakt
dodaje kontakt do kolekcji
kontakty
w obiekcie
o
jak i ustawia referencję
osoba
w obiekcie
k
.
Jeżeli chcemy utrwalić obiekt
Kontakt
w następnej transakcji, to trzeba mu ustawić odpowiednio referencje do obiektu
Osoba
. Przy założeniu, że będziemy dysponować tylko id osoby, to można wykonać następujące działania:
Kontakt k = new Kontakt("GG", "124234");
Osoba o = em.getReference(Osoba.class, osobaId);
k.setOsoba(o);
em.persist(k);
Dodam tylko, że manualne stworzenie instancji
Osoba
i ustawienie pola
id
nie zadziała.
Utrwalanie kaskadowe obiektów. Często istnieje potrzeba utrwalania całego drzewa obiektów, tj. obiektu nadrzędnego - korzenia drzewa (
Osoba
) i powiązanych z nim obiektów podrzędnych (
Kontakt
). JPA umożliwia zrobienie tego poprzez wykonanie operacji utrwalania na obiekcie nadrzędnym. Przy czym należy pamiętać o ustawieniu atrybut
cascade
na
CascadeType.PERSIST
w mapowaniu - tak jak zostało to zrobione w mapowaniu klasy
Osoba
dla property
kontakty
.
Działanie kaskadowe utrwalania obiektów można ustawić również po stronie obiektu podrzędnego, jednak wydaje się to być mało praktyczne.
2.4. Usuwanie obiektów z DB
Podobnie jak utrwalanie również i usuwanie obiektów może być rozdzielne lub kaskadowe.
Kaskadowe usuwanie obiektów jest analogiczne do kaskadowego utrwalania. Tzn. wykonując operację usunięcia obiektu głównego - korzenia drzewa obiektów, automatycznie zostaną usunięte powiązane z nim obiekty podrzędne. Tu należy pamiętać o ustawieniu atrybut
cascade
na
CascadeType.REMOVE
w mapowaniu - tak jak zostało to zrobione w mapowaniu klasy
Osoba
dla property
kontakty
. Implementacja JPA powinna usunąć obiekty podrzędne przed usunięciem obiektu nadrzędnego, żeby uniknąć naruszenia bazodanowych ograniczeń. Jednak usuwanie te nie musi być optymalne. Np hibernate'owa implementacja usuwa każdy obiekt z osobna po jego id, podczas gdy lepszym rozwiązaniem byłoby usunięcie wszystkich obiektów z jednej grupy jednym zapytaniem odwołując się do klucza obcego do bytu nadrzędnego.
Działania kaskadowego usuwania obiektów nie można ustawić po stronie obiektu podrzędnego.
W związku z usuwaniem obiektów podrzędnych w relacji jeden-do-wielu/wiele-do-jednego można wykorzystać rozszerzenie hibernate'owe - kaskadowe działanie
delete-orphan
. Otóż jeżeli w mapowaniu klasy nadrzędnej, kolekcję referencji do obiektów podrzędnych oznaczymy adnotacją
@Cascade( {org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
, to po usunięciu jakiegoś obiektu z tej kolekcji i utrwaleniu obiektu nadrzędnego, usunięty z kolekcji obiekt zostanie również usunięty z bazy danych.
o.getKontakty().remove(k);
em.persist(o);
Przy stosowaniu tego rozszerzenia należy pamiętać, że dla obiektu nadrzędnego choć raz utrwalonego, nie można podmieniać kolekcji obiektów podrzędnych, powiedzmy na nową instancje
ArrayList
, a próba utrwalenia takiego obiektu zwyczajnie się nie powiedzie. Jest tak, gdyż hibernate stosuje własne implementacje kolekcji -
persistence collections
, które pomagają w wykonaniu działania delete-orphan.
Usuwanie rozdzielne. Nie ma, żadnego problemu żeby usuwać obiekty podrzędne w sposób rozdzielny. Ponadto, bez użycia hibernate'owego rozszerzenia kaskadowego działania delete-orphan, jest to jedyny sposób aby z bazy danych pozbyć się pojedynczego obiektu podrzędnego.
Jednak zrezygnowanie z kaskadowego usuwania dla obiektów nadrzędnych jest mało atrakcyjne. Jeżeli jest założone ograniczenie bazodanowe, że klucz obcy do encji nadrzędnej nie może być pusty, to taka operacja w ogóle się nie powiedzie. Generalnie co się z tym stanie dalej, zależy od ustawień działań kaskadowych w bazie danych.
3. Jednokierunkowa relacja wiele-do-jednego
W porównaniu do dwukierunkowej relacji wiele-do-jednego/jeden-do-wielu, klasa
Kontakt
i jej mapowanie pozostaje niezmienione, za to z klasy
Osoba
zostanie usunięte powiązanie do klasy
Kontakt
.
Takie podejście uniemożliwia konfigurację działań kaskadowych (poza tymi określonymi w schemacie bazy danych) wyzwalanymi przez operacje na obiekcie
Osoba
. Ewentualnie można określić działania kaskadowe wyzwalane przez operacje na obiekcie
Kontakt
.
W omawianym przeze mnie przykładzie jednokierunkowa relacja wiele-do-jednego nie ma raczej praktycznego zastosowania.
4. Jednokierunkowa relacja jeden-do-wielu
W porównaniu do dwukierunkowej relacji wiele-do-jednego/jeden-do-wielu, to z klasy
Kontakt
zniknie powiązanie do klasy
Osoba
, natomiast klasa
Osoba
i jej mapowanie zmieni się następująco:
@Entity
@Table(name="osoba")
public class Osoba {
...
private List<Kontakt> kontakty;
@OneToMany(cascade={CascadeType.PERSIST, CascadeType.REMOVE})
@Cascade( {org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
@JoinColumn(name="oso_id", nullable=false)
public List<Kontakt> getKontakty() {
return kontakty;
}
public void dodajKontakt(Kontakt kontakt) {
if (null == kontakty) {
kontakty = new ArrayList<Kontakt>();
}
kontakty.add(kontakt);
}
}
Jak widać zmieniła się metoda
dodajKontakt
, która nie zawiera już instrukcji wiązania obiektu
Kontakt
z obiektem
Osoba
. Ale co najważniejsze zmieniło się nieco mapowanie. Ponieważ klasa
Kontakt
nie zawiera już mapowania tej relacji, to:
- zniknęła definicja atrybutu
mappedBy
w adnotacji
@OneToMany
;
- do mapowania property
kontakty
dodana została adnotacja
@JoinColumn
, która podobnie jak w klasie
Kontakt
służy do określenia kolumny w encji
Kontakt
, będącego kluczem obcym do encji
Osoba
. W przeciwnym wypadku domyślnie zostałaby użyta tabela pośrednicząca, jak w relacji wiele-do-wielu, o nazwie
[nazwa_tabeli_nadrzędnej]_[nazwa_tabeli_podrzędnej]
, która zawierałaby dwa klucze obce do tych tabel o następującym schemacie nazw
[nazwa_tabeli]_[nazwa_klucza_głównego]
.
Dla prezentowanego przeze mnie przykładu użycie jednokierunkowej relacji jeden-do-wielu jest całkiem rozsądnym podejściem. Jedyne co zostało utracone, to możliwość zapisu obiektów
Kontakt
niezależnie od obiektu
Osoba
. Utracona została również możliwość definiowania działań kaskadowych od strony obiektu
Kontakt
, ale do skorzystania z tej możliwości w tym przypadku i tak nie ma sensownej przesłanki.
5. Zasoby pomocnicze
Pliki źródłowe przykładu
JPA - pierwsze kroki
Hibernate Annotations - 2.2.5. Mapping entity bean associations/relationships
2.4. Hibernate Annotation Extensions
Specyfikacja JPA