Spring Boot – Spring Data JPA część II: Powiązania między tabelami

W pierwszej części postu, Spring Boot – Interakcja z bazą danych czyli Spring Data JPA, opisywałem jak się połączyć z bazą i jak stworzyć pierwszą encję. W tej części postaram się pokazać jak możemy zdefiniować powiązania pomiędzy encjami oraz jak możemy wykonywać zapytania z ich użyciem.

Cały kod z przykładów można znaleźć w repozytorium w GitHubie pod adresem https://github.com/mloza/spring-boot-database-II

Powiązania – ale po co?

Czasem musimy przedstawić w bazie bardziej złożone struktury które przechowują nie tylko typy proste ale również inne obiekty lub całe ich kolekcje. Przykładowo chcielibyśmy przechowywać wpisy na blogu oraz komentarze dotyczące każdego wpisu. W takim przypadku będziemy używać powiązania. W jednej tabelce zapiszemy posty, a w 2 komentarze. Następnie musimy tylko odpowiednio powiązać który komentarz należy do którego postu. Takie powiązanie nazywamy jeden do wielu (1:n) ponieważ mamy jeden post z wieloma komentarzami.

W sumie wyróżniamy 3 typy powiązań:

  • Wspomniane już jeden do wielu, na przykład jeden post ma wiele komentarzy
  • Jeden do jednego, przykładowo każdy wpis może (nie musi) mieć jeden obrazek tytułowy i każdy obrazek może być użyty tylko w jednym poście.
  • Wiele do wielu – przykładowo każdy post może mieć przypisanych wiele tagów oraz każdy tag może być przypisany do wielu postów

Relacja jeden do wielu

Relacja jeden do wielu (1:N) to najpopularniejszy typ relacji, dlatego od niej zacznę. Do zaprezentowania przykładów użyję kodu aplikacji z poprzedniego postu. Zdefiniowaliśmy tam encję Task, która reprezentowała zadanie do wykonania. Załóżmy, że chcielibyśmy oigrupować teraz te zadania w projekty, czyli każdy projekt będzie miał wiele zadań.

Powiązanie takie możemy osiągnąć poprzez dodanie dodatkowego pola do tabelki posiadającej wiele wierszy. Posłużmy się poprzednim przykładem z postem i komentarzami. Każdy post posiada unikalne pole id. Aby więc powiązać komentarz z postem, tabelka będzie zawierać pole post_id w którym umieścimy id powiązanego z nim postu. Dzięki temu możemy tworzyć wiele komentarzy które będą miały zapisane id postu z nim związanego.

Zacznijmy od stworzenia encji Project. Dla uproszczenia będzie ona posiadała tylko 2 pola: id oraz name. Definicja w kodzie będzie wyglądała następująco:

Następnie dodajmy repozytorium które dostarczy nam podstawowych operacji:

Jak do tej pory wszystko wygląda normalnie, tak jak to zostało opisane w poprzedniej części pliku. Jeśli do tej pory coś było dla Ciebie nowego, polecam przeczytać poprzednią część wpisu na temat Spring Data JPA.

Przejdźmy teraz do powiązania naszej encji Task z encją Project. W encji Task będziemy potrzebowali nowego pola z typem Project. Będzie ono reprezentowało projekt do którego należy nasze zadanie.

Tutaj pojawia się pierwsza nowość, adnotacja @ManyToOne która mówi nam, że przez to pole połączymy wiele zadań (Many) do (To) jednego projektu (One).

Mając już zdefiniowaną relację z encji Task do Project, powinniśmy zdefiniować teraz relację w 2 stronę. W encji Task dodajemy pole w następujący sposób:

Kolejna nowa adnotacja @OneToMany (czyli odwrotnie jak poprzednio @ManyToOne) mówiąca, że będziemy mieć jeden projekt (One) powiązany do (To) wielu zadań (Many). Dodatkowo musimy podać które pole mapuje relację w 2 stronę, czyli nazwa pola które tworzyliśmy w poprzednim kroku.

Do obydwu pól od razu tworzę gettery i settery które dla przejrzystości pomijam tutaj, ale będę z nich korzystał w późniejszych przykładach.

Jeśli uruchomimy teraz nasz projekt w konsoli zostaną wypisane zapytania które tworzą nasze relacje. Powinny wyglądać mniej więcej tak:

Zauważ, że w tabelce Task zostało dodane pole project_id w którym będziemy przechowywać numer projektu powiązanego z tym zadaniem. W ostatnim zapytaniu tworzymy powiązanie, dodajemy ograniczenie do tabelki Task które użyje klucza obcego project_id z tabelki Projects.

Zależnie od użytej bazy danych, zapytania te mogą się różnić od siebie. W podanym, przykładzie używam bazy H2.

Zmodyfikujmy też plik data.sql tab aby stworzyć od razu projekt i dodać do niego nasze zadania.

Listowanie i pobieranie powiązanych elementów

Przyszedł czas aby podjąć jakąś interakcję z naszą bazą i powiązanymi tabelkami z kodu. Zacznijmy od wypisania listy projektów i powiązanych z nimi zadań. W tym celu dodajmy nową metodę do klasy MainController.

Bardzo podobny kod omawialiśmy już w poprzedniej części wpisu. Jedyną nowością może być tutaj linia oznaczona jako // 1. Pobieramy w niej listę tasków, jest to zwykły getter, jednak jego wywołanie powoduje pobranie danych z powiązanej tabelki. Dla nas jest to przeźroczyste i wszystko zapewnia nam Spring Data JPA oraz Hibernate.

Możemy teraz przejść przeglądarką pod adres http://localhost:8080/project-tasks gdzie naszym oczom powinien ukazać się następujący widok:

Lista naszych projektów oraz powiązanych z nimi zadań

Tworzenie powiązań

Tworzenie nowych elementów w bazie mamy już opanowane. Jeśli je teraz chcemy powiązać, wystarczy ustawić pole na powiązany element. Zobaczmy przykład:

Trzeba pamiętać jedynie, aby dodać powiązanie w 2 strony. Czyli ustawiamy pole Project na wybrany projekt (// 1) oraz dodajemy zadanie do listy zadań przechowywanej w projekcie (// 2). Jest to wymagane aby zachować spójność wszystkich obiektów. W wielu przypadkach jeśli ustawimy relację tylko w jedną stronę, nie zostanie ona odzwierciedlona w bazie danych, mimo że, nie pojawi się nam żaden błąd.

Jeśli przejdziemy teraz pod adres http://localhost:8080/add-task powinniśmy zobaczyć taką samą listę jak w poprzednim kroku wraz z nową pozycją na liście zadań:

Lista zadań z nowym elementem przypisanym do naszego projektu

Mamy już opanowane listowanie i tworzenie nowych elementów wraz z powiązaniami. Czas teraz na usuwanie.

Usuwanie elementów powiązanych

Przy usuwaniu, podobnie jak przy tworzeniu, musimy pamiętać aby usunąć powiązanie z obu stron. Czyli w naszym przykładzie usuwamy zadanie z listy w projekcie, a następnie usuwamy to zadanie.

Takie usuwanie jest najbezpieczniejsze i pozwoli nam zachować spójność obiektów i stanu bazy.

Jeśli jednak chcemy usunąć Projekt, musimy najpierw pozbyć się zadań z nim powiązanych. Możemy to osiągnąć poprzez usunięcie tych zadań wcześniej, przypisanie ich do innego projektu lub usunięcie projektu (przypisanie nulla do pola projekt – domyślnie podanie wartości tego pola jest opcjonalne, jeśli ma być wymagane należy to ustawić w adnotacji @ManyToOne).

Jeżeli nie dopełnimy tych wymagań, możemy ujrzeć taka stronę błędu:

Strona błędu usunięcia wpisu posiadającego powiązane wpisy w innej tabelce.

Powiązanie OneToOne

Powiązanie jeden do jednego (1:1) jest reprezentowana w bazie tak samo jak jeden do wielu, jednak tutaj, w połączonej tabeli znajduje się tylko jeden rekord.

Takiego powiązania możemy użyć gdy mamy jakiś element który występuje tylko sporadycznie i nie koniecznie chcemy go mieć ew tej samej tabeli. Przykładowo, możemy dodać szczegóły projektu zawierające szerszy opis.

Zacznijmy od stworzenia Encji która będzie przechowywała nam szczegółowe informacje na temat projektu. Na ten moment encja ta będzie posiadała jedynie pole z opisem naszego projektu.

Aby utworzyć powiązanie między tabelkami musimy dodać odpowiednie pola w obu klasach. W klasie Project details dodajemy odnośnik do projektu:

A w klasie Project powiązanie z ProjectDetails:

I to wszystko. Mamy juz powiązanie. Aby wykonać wykonać jakieś operacje, musimy stworzyć jeszcze repozytorium dla ProjectDetails:

Wszystkie operacje są analogiczne jak w połączeniu ManyToOne, dlatego je tutaj pominę.

Powiązanie ManyToMany

Ostatnim typem powiązań jest powiązanie wiele do wielu (n:m). Stosuje się je kiedy chcemy połączyć wzajemnie różne wpisy, przykładowo post na blogu może mieć przypisane wiele tagów, ale każdy tak może być przypisany do wielu postów. Jest to najbardziej skomplikowany typ powiązań. Wymaga on dodatkowej tabelki pośredniej w której będziemy zapisywać który wpis z jednej tabelki jest połączony z drugim wpisem w innej tabelce. Dzięki temu możemy mieć wiele rekordów opisujących nasze powiązania.

Idąc za wcześniejszym przykładem, dodajmy możliwość tagowania projektów w naszej aplikacji. Do każdego projektu będziemy mogli przypisać wiele tagów, ale i do każdego tagu będziemy mogli przypisać wiele projektów. Zacznijmy od stworzenia encji Tag.

Od razu stwórzmy Repozytorium dla tej encji:

Teraz musimy stworzyć powiązanie poprzez dodanie odpowiednich pól w encjach Tag i Project. Zacznijmy od encji Tag, dodajmy pole:

Następnie w encji Project dodajemy powiązanie w 2 stronę:

Tworzenie powiązań

Wszystkie operacje wykonujemy bardzo podobnie jak w poprzednich powiązaniach, z tą różnicą, że tutaj w obu przypadkach mamy listy. Stwórzmy dla przykładu 2 projekty i 2 tagi. Pierwszy projekt będzie miał przypisany tylko jeden tag, a 2 projekt oba.

Po przejściu na stronę http://localhost:8080/tagging powinniśmy w przeglądarce zobaczyć napis OK, a w konsoli logi:

Jak widać, powiązania zostały zapisane w tabelce tag_projects. Wszystko wygląda poprawnie. Spróbujmy teraz wylistować wszystkie projekty i przypisane do nich tagi.

Kod jest bardzo prosty. Wyciągamy wszystkie projekty z bazy (//1), iterujemy po nich i dodajemy nazwy do wyjścia, następnie wyciągamy wszystkie powiązane tagi (//2) i wypisujemy je pod projektami. Po przejściu na stronę http://localhost:8080/by-project powinniśmy zobaczyć następujący tekst.

Powiązanie działa w 2 strony, możemy w ten sam sposób wylistować tagi i powiązane projekty.

Przejście na stronę http://localhost:8080/by-tag powinniśmy zobaczyć listę tagów wraz z projektami. Mniej więcej powinno to wyglądać następująco:

Usuwanie powiązań

Przy usuwaniu powiązań, musimy je usunąć obustronnie. Załóżmy, że chcemy usunąć Tag 1 z Project 2

Po przejściu na stronę http://localhost:8080/remove-tag w logu powinniśmy znaleźć zapytanie usuwające powiązanie.

Podobnie jak w przypadku relacji jeden-do-wielu, nie możemy usunąć wpisu który ma istniejące powiązania. Przed wykasowaniem takiego rekordu należy usunąć najpierw wszystkie powiązania.

Podsumowanie

W poście zaprezentowałam 3 podstawowe powiązania. Za ich pomocą możemy wyrazić większość potrzebnych nam w aplikacji powiązań. Hibernate i JPA pozwalają nam zdefiniować jeszcze kilka innych powiązań pomiędzy obiektami, które wykraczają ponad temat tego wpisu, jednak końcowo sprowadza się to do wykorzystania opisanych powiązań.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

This site uses Akismet to reduce spam. Learn how your comment data is processed.