Zacznę od tego, że najkrótszą odpowiedzią na to pytanie będzie: „wtedy kiedy jej potrzebujesz”. Oczywiście wszystko sprowadza się do tego, kiedy będziemy takiej relacji potrzebować. Postaram się przybliżyć to w tym artykule. Zacznę od tego, co to jest relacja jedno i dwukierunkowa w Hibernate i jak ją prawidłowo skonfigurować.
To czy relacja jest jedno, czy dwukierunkowa zależy od konfiguracji. Jeśli skonfigurujemy relację tylko po jednej stronie (w jednej z powiązanych encji), to wtedy taka relacja będzie jednokierunkowa. Jeśli skonfigurujemy relację w obu powiązanych encjach, to tak relacja będzie dwukierunkowa.
Co daje nam relacja dwukierunkowa (bidirectional)?
Relacja dwukierunkowa daje nam możliwość przeglądania encji po obu stronach relacji, bez wykonywania dodatkowych zapytań. Więc jeśli pobierzemy sobie np. jednego posta wraz z komentarzami przypisanymi do niego (relacja one-to-many), to po stronie komentarzy, będziemy mieli dostęp do obiektu tego posta (relacja dwukierunkowa). W relacji jednokierunkowej w encji Comment
nie będziemy mieli już dostępu do tego posta (oczywiście możemy przekazać tego posta w jakiś inny sposób, ale musimy to robić ręcznie).
W relacji one-to-many musisz pamietać o adnotacj
@JoinColumn
, która wskazuje kolumnę mapowania relacji w tabeli po stroniemany
w naszym wypadku w tabelicomment
. Bez tej adnotacji Hibernate będzie oczekiwał tabeli łączącejpost_comment
. Jeśli używasz Hibernate’a do aktualizowania schematu bazy danych, to Hibernate wygeneruje sobie taką tabelę. Mapowanie one-to-many z tabelą łączącą jest dużo mniej wydajne niż mapowanie z użyciem dodatkowej kolumny klucza obcego. Nie powinieneś mapować tej relacji w taki sposób, chyba że naprawdę wiesz co robisz.
Przykładowa konfiguracja relacji one-to-many jednokierunkowej:
public class Post { @Id private Long id; private String title; private String content; @OneToMany @JoinColumn(name = "post_id") private List<Comment> comments; // ... gettery i settery ... } public class Comment { @Id private Long id; private String content; // ... gettery i settery ... }
Baza danych dla takiego przykładu wyglądałaby mniej więcej tak jak na poniższym obrazku.
W tym miejscu, gdy pobierzemy sobie posta i chcemy przeiterować po komentarzach, to z poziomu komentarza nie jesteśmy w stanie odwołać się w łatwy sposób do posta.
Post post = em.find(1L, Post.class).getSingleResult(); for(Comment comment : post.getComments()) { log.info(comment.getPost()) //Błąd. Nie ma takiej metody, nie ma pola post }
Oczywiście mamy dostęp do referencji post
w tym miejscu. Ale gdybyśmy chcieli np. przekazać listę komentarzy do jakiejś metody, lub metod i potrzebowali informacji o poście to wszędzie musielibyśmy przekazywać jako kolejny parametr post
. Co jest trochę uciążliwe i powoduje, że mamy w metodach dodatkowe parametry. Możemy tego uniknąć poprzez skonfigurowanie relacji jako dwukierunkowej.
public class Post { @Id private Long id; private String title; private String content; @OneToMany(mappedBy = "post") // zmieniamy mapowanie private List<Comment> comments; // ... gettery i settery ... } public class Comment { @Id private Long id; private String content; @ManyToOne(fetch = FetchType.LAZY) // dodajemy pole post i mapujemy je poprzez adnotację private Post post; // ... gettery i settery ... }
W klasie Comment
dodajemy pole Post post
mapujemy je @ManyToOne
, warto tutaj także ustawić fetch = FetchType.LAZY
z tego względu, że domyślnie w tej adnotacji fetchType
jest równy EAGER
.
Kolejna rzecz, jaką musimy skonfigurować to zmiany mapowania nad polem comments
w klasie Post
. Tutaj usuwamy adnotację @JoinColumn
i poprzez parametr mappedBy
wskazujemy pole w klasie Comment
, które mapuje naszą relację. Te dwie konfiguracje różnią się między sobą tylko na poziomie kodu (po stronie bazy danych nie ma takiego rozróżnienia).
I w takim wypadku z poziomu komentarza możemy już w prosty sposób odwołać się do obiektu Post
.
Post post = em.find(1L, Post.class).getSingleResult(); for(Comment comment : post.getComments()) { log.info(comment.getPost().getTitle()); }
Zalety relacji dwukierunkowej
To, że mamy łatwy dostęp do referencji posta, do którego przypisany jest komentarz, przydaje się jeszcze w sytuacji, gdy chcemy pobrać listę komentarzy.
List<Comment> comment = em.createQuery("SELECT c from Comment", Post.class).getResulList(); for(Comment comment : comment) { log.info(comment.getPost().getTitle()); log.info(comment.getContent()); }
Oczywiście takie zapytanie SELECT c from Comment
pobierze wszystkie komentarze, które masz w bazie danych (mogą być ich tysiące). Poza tym pojawią się dodatkowe zapytania dla pobrania postów powiązanych z tymi komentarzami (jedno dla każdego posta). Tak właśnie działa ta funkcjonalność.
Kolejna zaleta. Gdybyśmy chcieli dodać komentarz w relacji jednokierunkowej, to Hibernate najpierw doda komentarz, a później zaktualizuje dodatkowym zapytanie update
kolumnę klucza obcego (post_id). W przypadku usuwania będzie trochę inaczej (ale podobnie), ponieważ najpierw zapytaniem update
wyczyści kolumnę klucza obcego (post_id), a dopiero później i to w dodatku tylko jeśli mamy ustawioną flagę orphanRemoval = true
, wykona zapytanie delete
. Dzieje się tak dlatego, że encja Comment
nie przechowuje żadnej informacji o kluczu obcym (kolumnie post_id). Natomiast w przypadku relacji dwukierunkowej taki problem nie istnieje, nie wykonują się dodatkowe zapytania.
Wady relacji dwukierunkowej
Oczywistą wadą jest to, że musimy dodać dodatkowe pole do skonfigurowania takiej relacji. Nie jest to oczywiście nic strasznego, ale jednak dwie dodatkowe linijki kodu i do tego getter i setter (jeśli potrzebujesz). Czasem encja jest powiązana z kilkoma innymi encjami, więc takim pól może być całkiem sporo.
Kolejna wada. Przy operacjach takich jak dodawanie i usuwanie, trzeba pamiętać, o tym, żeby synchronizować taką relację po obu stronach. Np. jeśli chcesz przypisać komentarz do innego posta, to najpierw musisz go dodać do listy w poście, a później musisz przypisać posta do tego komentarza. Zwykle dodaje się w tym celu metody pomocnicze. W naszym wypadku w klasie Post
lub Comment
.
public class Post { @Id private Long id; private String title; private String content; @OneToMany(mappedBy = "post") private List<Comment> comments; // ... gettery i settery ... // metoda pomocnicza public void addComment(Comment comment) { comments.add(comment); comment.setPost(this); } }
Kiedy konfigurować relację dwukierunkową?
Nie zawsze jest potrzeba konfigurowania relacji dwustronnej, ponieważ często dzieje się tak, że odczytujemy encje tylko po jednej stronie relacji. Trochę nadmiarowe jest w takiej sytuacji konfigurowanie czegoś, czego nie będziemy używali. Dodatkowo jeśli nie wykonujemy żadnych operacji poza odczytaniem takiej relacji, to właściwie konfigurowanie relacji dwustronnej nie daje nam żadnych korzyści.
W programowaniu często kieruję się zasadą, która mówi, żeby nie robić rzeczy na zapas (YAGNI – You aren’t gonna need it), bo być może nigdy nie będziesz ich używał, a dodatkowy, nieużywany kod, tylko zaciemnia całość.
Jeśli masz np. encję klienta i encję adresu połączona relacją jeden do wielu, to raczej nie będziesz odczytywał adresów w oderwaniu od klienta. I w tym wypadku wystarczy relacja jednostronna.
Jeśli masz klientów i ich zamówienia to raczej będziesz potrzebował relacji po obu stronach (dwukierunkowej). Czasem będziesz pobierał klientów z ich zamówieniami, a czasem zamówienia z klientami (np. tam, gdzie chcesz procesować zamówienia).
W przypadku gdy dodajesz lub usuwasz encje do powiązanych kolekcji, to lepiej jest stosować relację dwustronną, unikniesz wtedy dodatkowych zapytań. Warto tutaj pamiętać o włączeniu logowania zapytań, co pomaga wykrywać takie sytuacje.
Jeśli nie wiesz kiedy zastosować relację dwukierunkową, a kiedy tylko jedno kierunkową, to raczej powinieneś zaczynać od relacji jedno kierunkowej. Dopiero jak będzie potrzeba skorzystania z relacji po drugiej stronie, wtedy dorobisz relację dwukierunkową.
Podsumowanie
Różnice pomiędzy relacją jedno i dwukierunkową jest tak naprawdę niewielka i występuje ona jedynie po stronie kodu. Nie należy więc bać się relacji dwukierunkowej, nie powoduje ona wolniejszego działania aplikacji, czy jakichś innych niepożądanych zachowań. Natomiast sprawia ona, że kod jest troszeczkę bardziej skomplikowany.
Rzecz, o której trzeba zawsze pamiętać, to synchronizacja tej relacji po obu stronach (gdy dodajemy lub usuwamy powiązane encje).
Jeśli potrzebujesz jeszcze więcej wiedzy, z zakresu Hibernate to zapraszam Cię też do mojego kursu Hibernate, w którym znajdziesz kompleksowe omówienie wielu zagadnień związanych z Hibernate. Od najbardziej podstawowych tematów do bardziej zaawansowanych zagadnień.
Moje dotychczasowe doświadczenia pokazują mi, że wiązanie dwukierunkowe rzadko kiedy jest niezbędne do życia. Gdybym miał procentowo określić ilość wiązań jednokierunkowych do dwukierunkowych to byłoby to ok. 85% jednokierunkowych i tylko 15% dwukierunkowych.
Dzięki za komentarz Krzysztof. W sumie nie zastanawiałem się, jak to może wyglądać procentowo. Ale wydawało mi się, że to raczej będzie 50/50 lub 60/40 coś w tych granicach. Może to zależy od skomplikowania aplikacji, nad którymi pracujemy?
U mnie w projekcie wygląda to mniej więcej 80/20. oczywiscie dla relacji jednokierunkowej. Relacje dwukierunkowe implamentujemy tylko kiedy jest taka potrzeba.
Dzięki za komentarz Robert 😉
Długo „chodziło” mi po głowie aby wyjaśnić te różnice. Aż dostałem maila z tym artykułem.
Jasno i konkretnie opisane.
Dzięki za to co robisz Mateusz!
Dzięki Michał 😉
W której z encji wykorzystujemy mappedBy, a w której joinTable przy relacji dwukierunkowej? Jak wybrać poprawnie?
Przy relacji dwukierunkowej zawsze stosujemy mappedBy, bo wtedy mapujesz relację po obu stronach, więc musisz wskazać właściciela (właśnie przez mappedBy). Przy relacji jedno kierunkowej stosujemy JoinColumn, bo masz mapowanie tylko po jednej stronie (więc wskazujesz, która kolumna w bazie to mapuje).