niedziela, 27 kwietnia 2008

JPA - relacje jeden-do-wielu i wiele-do-jednego

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

5 komentarzy:

Unknown pisze...

Podczas przygotowywania do egzaminu SCBCD5 bardzo intensywnie analizowałem specyfikację JPA, ale jakimś cudem umknęła mi pewna cecha mapowania OneToMany, którą przyszło mi zastosować niedawno - dwukierunkowa relacja, która odwzorowana jest na zasadzie klucza obcego w tabeli encji podrzędnej (Tworzenie aplikacji desktopowej z Java Desktop Application w NetBeans IDE 6.1). Wydaje mi się, że niezbyt jasno uwypukliłeś tę cechę, która nieweluje stosowanie tabeli pośredniej łączącej dwie tabele encji za pomocą kluczy obcych, a pozwala na skorzystanie z netbeans'owego typu projektu Java Desktop Application. Długo trwało zanim zrozumiałem to działanie w JPA i sądzę, że notka o tym, jako kontynuacja tego wpisu byłaby wielce porządana przez czytelników Twojego bloga. Chętnie poczytałbym o tym aspekcie, jak to wygląda z Twojej perspektywy. Da się? ;-)

Jacek
Notatnika Projektanta Java EE

Dawid Walczak pisze...

Hmm.. W zasadzie przez cały powyższy artykuł przewija się, że encja podrzędna jest właścicielem relacji i posiada klucz obcy do encji nadrzędnej - w mniej lub bardziej jawny sposób. Natomiast tylko raz (w pkt 4) zdawkowo zaznaczyłem o tym, że JPA użyłoby tabeli pośredniej.
Wydaje mi się więc, że nawet nazbyt intensywnie opierałem się na wspomnianej przez Ciebie cesze.
Dziękuje za komentarz.

44 pisze...

Prosto i na temat dzięki

44 pisze...

Witam tego szukałem, i nad tym się zastanawiałem nie mogłem tego nigdzie znaleźć:
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.


Mam też takie pytanie czy nie lepiej od razu zainicjalizować listę kontakt, przecież nie możemy utworzyć encji osoba która nie miałaby przynajmniej 1 kontaktu dlatego chyba lepiej jest od razu zainicjalizować liste:

public List contacts = new ArrayList();

A tak to musimy za każdym razem sprawdzać if(contacts != null)

Po drugie takie pytanie czy nie możemy dodać do encji Osoba takiego konstruktora :

public Osoba(List kontakts){
for(Kontakt k : kontakts){
k.setOsoba(this);
}
this.kontakts = kontakts;
}

Jak widzę szukając informacji na temat dwukierunkowej relacji jeden do wielu, to dużo programistów nawet z dużym stażem ma z tą relacją problemy. Bardzo dobrze, że poruszył Pan ten problem. Pozdrawiam

CamilYedrzejuq pisze...

Hej jak zdefiniować takie powiązanie w BD.
Mało kto wie, że można zrobić powiązanie na zasadzie część-całość(kompozycja) w BD.
Mamy np.

https://www.dropbox.com/s/2y57t4ea2pvumzv/RozgrywkiPi%C5%82karskie.jpg?dl=0

Rozgrywka-TypRozgrywki.
Rozgrywka nie może istnieć bez typu. Lepszy przykład to Faktura-PozycjaFaktury
https://www.dropbox.com/s/8xp2l3d6ecxyp8n/Faktury.jpg?dl=0

O ile ktoś z Was kojarzy projektowanie baz z użyciem modelu konceptualnego.
http://infocenter.sybase.com/archive/index.jsp?topic=/com.sybase.stf.powerdesigner.docs_12.1.0/html/cdug/cdugp86.htm

Napisałeś o kompozycji w modelu obiektowym między klasami User i Address ale model relacyjny już tego nie odwzorowuje tak jak to powinno być w 100% poprawnie.
Address nie może istnieć bez Użytkownika.

http://chomikuj.pl/michal2229/Studia/WAT+Informatyka+2/semestr+III/Bazy+danych/Bazy+danych_L1,3811596983.ppt slajd 50.