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
Wpis jest częścią serii wpisów na temat Spring Boot. Zapraszam do zapoznania się z pozostałymi wpisami na blogu!
- Spring Boot – szybkie tworzenie aplikacji web w Javie
- Spring Boot – Interakcja z bazą danych czyli Spring Data JPA
- Spring Boot – Widoki
- Użycie Spring Security w Spring Boot
- Spring Boot Web – Przekazywanie zmiennych do aplikacji przez URL czyli użycie @RequestParam i @PathVariable
- Spring Boot Test – testowanie aplikacji w Spring Boocie
- Wsparcie dla pola typu JSONB w PostgreSQL dla Spring Data JPA + Hibernate
- Wysyłanie plików na serwer przez Spring Boot
- Spring Boot – Spring Data JPA część II: Powiązania między tabelami
- Spring Mail + Spring Boot – łatwe wysyłanie maili z aplikacji w Javie
- Docker + Spring Boot – zamykamy aplikację w kontenerze Dockerowym
- Java Bean Validation + Spring Boot – sprawdzanie poprawności danych w Spring Boocie
- Spring Boot + Swagger UI
- Serwer Amazon EC2 i SSL od Let’s encrypt w aplikacji Spring Boot
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 pogrupować 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:
@Entity | |
public class Project { | |
@GeneratedValue | |
@Id | |
private int id; | |
@Column | |
private String name; | |
public int getId() { | |
return id; | |
} | |
public void setId(int id) { | |
this.id = id; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
@Override | |
public String toString() { | |
return "Project{" + | |
"id=" + id + | |
", name='" + name + '\'' + | |
"}"; | |
} | |
} |
Następnie dodajmy repozytorium, które dostarczy nam podstawowych operacji:
public interface ProjectRepository extends CrudRepository<Project, Integer> { } |
Tworzenie powiązania
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.
@ManyToOne | |
private Project project; |
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 Project dodajemy pole w następujący sposób:
@OneToMany(mappedBy = "project") | |
private List<Task> tasks; |
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:
Hibernate: create table project (id integer generated by default as identity, name varchar(255), primary key (id))
Hibernate: create table task (id bigint generated by default as identity, budget double, description clob, done boolean, name varchar(255), project_id integer, primary key (id))
Hibernate: alter table task add constraint FK_b7i81l1tk1ph95xnhtoftyv53 foreign key (project_id) references project
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.
INSERT INTO project(name) VALUES('Projekt 1');
INSERT INTO task(name, description, budget, done, project_id) VALUES('Task 1', 'Description of task 1', 100.00, 1, SELECT id FROM project WHERE name = 'Projekt 1');
INSERT INTO task(name, description, budget, done, project_id) VALUES('Task 2', 'Description of task 2 Do', 100.00, 1, SELECT id FROM project WHERE name = 'Projekt 1');
INSERT INTO task(name, description, budget, done, project_id) VALUES('Task 3', 'Description of task 3 Do', 50.00, 0, SELECT id FROM project WHERE name = 'Projekt 1');
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.
@Autowired | |
public ProjectRepository projectRepository; | |
@RequestMapping("/project-tasks") | |
@ResponseBody | |
public String projectsAndTasks() { | |
StringBuilder response = new StringBuilder(); | |
for(Project project: projectRepository.findAll()) { | |
response.append(project).append("<br>"); | |
for(Task task: project.getTasks()) { // 1 | |
response.append("- ").append(task).append("<br>"); | |
} | |
} | |
return response.toString(); | |
} |
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:
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:
@RequestMapping("/add-task") | |
@ResponseBody | |
public String addTask() { | |
Project project = projectRepository.findOne(1); | |
Task task = new Task() | |
.withBudget(12.00) | |
.withDescription("New task") | |
.withName("New task") | |
.withProject(project); // 1 | |
project.getTasks().add(task); // 2 | |
taskRepository.save(task); | |
return projectsAndTasks(); | |
} |
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ń:
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.
@RequestMapping("/delete-task") | |
@ResponseBody | |
public String deleteTask() { | |
Task task = taskRepository.findOne(1L); | |
Project project = task.getProject(); | |
project.getTasks().remove(task); | |
taskRepository.delete(task); | |
return projectsAndTasks(); | |
} |
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ć taką stronę błędu:
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.
@Entity | |
public class ProjectDetails { | |
@GeneratedValue | |
@Id | |
private int id; | |
@Column | |
private String description; | |
// pomijam gettery i settery dla czytelnoÅci | |
@Override | |
public String toString() { | |
return "ProjectDetails{" + | |
"id=" + id + | |
", description='" + description + '\'' + | |
'}'; | |
} | |
} |
Aby utworzyć powiązanie między tabelkami, musimy dodać odpowiednie pola w obu klasach. W klasie Project details dodajemy odnośnik do projektu:
@OneToOne | |
Project project; |
A w klasie Project powiązanie z ProjectDetails:
@OneToOne(mappedBy = "project") | |
private ProjectDetails projectDetails; |
I to wszystko. Mamy już powiązanie. Aby wykonać jakieś operacje, musimy stworzyć jeszcze repozytorium dla ProjectDetails:
public interface ProjectDetailsRepository extends CrudRepository<ProjectDetails, Integer> { } |
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.
@Entity | |
public class Tag { | |
@GeneratedValue | |
@Id | |
private int id; | |
private String name; | |
// Gettery i settery | |
} |
Od razu stwórzmy Repozytorium dla tej encji:
public interface TagRepository extends CrudRepository<Tag, Integer> { } |
Teraz musimy stworzyć powiązanie poprzez dodanie odpowiednich pól w encjach Tag i Project. Zacznijmy od encji Tag, dodajmy pole:
@ManyToMany | |
private List<Project> projects; |
Następnie w encji Project dodajemy powiązanie w 2 stronę:
@ManyToMany(mappedBy = "projects") | |
private List<Tag> tags; |
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:
Hibernate: insert into project (id, name) values (null, ?)
Hibernate: insert into project (id, name) values (null, ?)
Hibernate: insert into tag (id, name) values (null, ?)
Hibernate: insert into tag_projects (tags_id, projects_id) values (?, ?)
Hibernate: insert into tag_projects (tags_id, projects_id) values (?, ?)
Hibernate: insert into tag (id, name) values (null, ?)
Hibernate: insert into tag_projects (tags_id, projects_id) values (?, ?)
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.
project: Projekt 1
project: Project 1
- Tag 1
project: Project 2
- Tag 1
- Tag 2
Powiązanie działa w 2 strony, możemy w ten sam sposób wylistować tagi i powiązane projekty.
@RequestMapping("/by-tag") | |
@ResponseBody | |
public String projectsByTag() { | |
StringBuilder out = new StringBuilder(); | |
tagRepository.findAll().forEach(tag -> { | |
out.append("tag: ").append(tag.getName()).append("<br>"); | |
tag.getProjects().forEach(project -> { | |
out.append("- ").append(project.getName()).append("<br>"); | |
}); | |
}); | |
return out.toString(); | |
} |
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:
tag: Tag 1
- Project 1
- Project 2
tag: Tag 2
- Project 2
Usuwanie powiązań
Przy usuwaniu powiązań, musimy je usunąć obustronnie. Załóżmy, że chcemy usunąć Tag 1 z Project 2
@RequestMapping("/remove-tag") | |
@ResponseBody | |
public String removeTag() { | |
Optional<Project> project = | |
StreamSupport | |
.stream(projectRepository | |
.findAll() | |
.spliterator(), false) | |
.filter(p -> p.getName().equals("Project 2")) | |
.findAny(); | |
project.ifPresent(p -> { | |
Optional<Tag> tag = p.getTags().stream() | |
.filter(t -> t.getName().equals("Tag 2")) | |
.findAny(); | |
tag.ifPresent(t -> { | |
t.getProjects().remove(p); | |
p.getTags().remove(t); | |
tagRepository.save(t); | |
projectRepository.save(p); | |
}); | |
}); | |
return "OK"; | |
} |
Po przejściu na stronę http://localhost:8080/remove-tag w logu powinniśmy znaleźć zapytanie usuwające powiązanie.
Hibernate: delete from tag_projects where tags_id=?
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ń.
Wpis jest częścią serii wpisów na temat Spring Boot. Zapraszam do zapoznania się z pozostałymi wpisami na blogu!
- Spring Boot – szybkie tworzenie aplikacji web w Javie
- Spring Boot – Interakcja z bazą danych czyli Spring Data JPA
- Spring Boot – Widoki
- Użycie Spring Security w Spring Boot
- Spring Boot Web – Przekazywanie zmiennych do aplikacji przez URL czyli użycie @RequestParam i @PathVariable
- Spring Boot Test – testowanie aplikacji w Spring Boocie
- Wsparcie dla pola typu JSONB w PostgreSQL dla Spring Data JPA + Hibernate
- Wysyłanie plików na serwer przez Spring Boot
- Spring Boot – Spring Data JPA część II: Powiązania między tabelami
- Spring Mail + Spring Boot – łatwe wysyłanie maili z aplikacji w Javie
- Docker + Spring Boot – zamykamy aplikację w kontenerze Dockerowym
- Java Bean Validation + Spring Boot – sprawdzanie poprawności danych w Spring Boocie
- Spring Boot + Swagger UI
- Serwer Amazon EC2 i SSL od Let’s encrypt w aplikacji Spring Boot