Java-Persistenz mit JPA und Hibernate, Teil 2: Viele-zu-viele-Beziehungen
In der ersten Hälfte dieses Lernprogramms wurden die Grundlagen der Java Persistence API vorgestellt und gezeigt, wie Sie eine JPA-Anwendung mit Hibernate 5.3.6 und Java 8 konfigurieren. Wenn Sie dieses Lernprogramm gelesen und die Beispielanwendung studiert haben, kennen Sie die Grundlagen von Modellierung von JPA-Entitäten und Viele-zu-Eins-Beziehungen in JPA. Sie haben auch einige Übungen zum Schreiben benannter Abfragen mit JPA Query Language (JPQL) durchgeführt.
In dieser zweiten Hälfte des Tutorials werden wir uns eingehender mit JPA und Hibernate befassen. Sie lernen, wie Sie eine Viele-zu-Viele-Beziehung zwischen Movie
und SuperHero
Entitäten modellieren , individuelle Repositorys für diese Entitäten einrichten und die Entitäten in der speicherinternen H2-Datenbank speichern. Außerdem erfahren Sie mehr über die Rolle von Kaskadenoperationen in JPA und erhalten Tipps zur Auswahl einer CascadeType
Strategie für Entitäten in der Datenbank. Schließlich stellen wir eine funktionierende Anwendung zusammen, die Sie in Ihrer IDE oder in der Befehlszeile ausführen können.
Dieses Tutorial konzentriert sich auf die Grundlagen von JPA. Lesen Sie jedoch unbedingt diese Java-Tipps, in denen fortgeschrittenere Themen in JPA vorgestellt werden:
- Vererbungsbeziehungen in JPA und Hibernate
- Zusammengesetzte Schlüssel in JPA und Ruhezustand
Viele-zu-viele-Beziehungen in JPA
Viele-zu-viele-Beziehungen definieren Entitäten, für die beide Seiten der Beziehung mehrere Verweise aufeinander haben können. In unserem Beispiel werden wir Filme und Superhelden modellieren. Im Gegensatz zum Beispiel "Autoren und Bücher" aus Teil 1 kann ein Film mehrere Superhelden haben, und ein Superheld kann in mehreren Filmen erscheinen. Unsere Superhelden Ironman und Thor sind beide in zwei Filmen zu sehen, "The Avengers" und "Avengers: Infinity War".
Um diese Viele-zu-Viele-Beziehung mithilfe von JPA zu modellieren, benötigen wir drei Tabellen:
- FILM
- SUPERHELD
- SUPERHERO_MOVIES
Abbildung 1 zeigt das Domänenmodell mit den drei Tabellen.

Beachten Sie, dass dies SuperHero_Movies
eine Verknüpfungstabelle zwischen den Tabellen Movie
und ist SuperHero
. In JPA ist eine Join-Tabelle eine spezielle Art von Tabelle, die die Viele-zu-Viele-Beziehung erleichtert.
Unidirektional oder bidirektional?
In JPA verwenden wir die @ManyToMany
Annotation, um viele-zu-viele-Beziehungen zu modellieren. Diese Art von Beziehung kann unidirektional oder bidirektional sein:
- In einer unidirektionalen Beziehung zeigt nur eine Entität in der Beziehung auf die andere.
- In einer bidirektionalen Beziehung zeigen beide Entitäten aufeinander.
Unser Beispiel ist bidirektional, was bedeutet, dass ein Film auf alle seine Superhelden und ein Superheld auf alle ihre Filme zeigt. In einem bidirektionalen, viele-zu-viele - Beziehung, eine Einheit besitzt , die Beziehung und die andere zu kartiert die Beziehung. Wir verwenden das mappedBy
Attribut der @ManyToMany
Annotation, um diese Zuordnung zu erstellen.
Listing 1 zeigt den Quellcode für die SuperHero
Klasse.
Listing 1. SuperHero.java
package com.geekcap.javaworld.jpa.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @Entity @Table(name = "SUPER_HERO") public class SuperHero { @Id @GeneratedValue private Integer id; private String name; @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinTable( name = "SuperHero_Movies", joinColumns = {@JoinColumn(name = "superhero_id")}, inverseJoinColumns = {@JoinColumn(name = "movie_id")} ) private Set movies = new HashSet(); public SuperHero() { } public SuperHero(Integer id, String name) { this.id = id; this.name = name; } public SuperHero(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set getMovies() { return movies; } @Override public String toString() { return "SuperHero{" + "id=" + id + ", + name +"\'' + ", + movies.stream().map(Movie::getTitle).collect(Collectors.toList()) +"\'' + '}'; } }
Die SuperHero
Klasse enthält einige Anmerkungen, die aus Teil 1 bekannt sein sollten:
@Entity
identifiziert sichSuperHero
als JPA-Entität.@Table
ordnet dieSuperHero
Entität der Tabelle "SUPER_HERO" zu.
Beachten Sie auch das Integer
id
Feld, das angibt, dass der Primärschlüssel der Tabelle automatisch generiert wird.
Als nächstes schauen wir uns die @ManyToMany
und @JoinTable
Anmerkungen an.
Strategien abrufen
In der @ManyToMany
Anmerkung ist zu beachten, wie wir die Abrufstrategie konfigurieren , die faul oder eifrig sein kann. In diesem Fall haben wir die Menge fetch
zu EAGER
, so dass , wenn wir ein Abrufen SuperHero
aus der Datenbank, werden wir auch alle seine entsprechenden automatisch abrufen Movie
s.
Wenn wir LAZY
stattdessen einen Abruf durchführen würden, würden wir jeden nur abrufen, Movie
wenn speziell darauf zugegriffen wurde. Faules Abrufen ist nur möglich, wenn das SuperHero
an das angehängt ist EntityManager
; Andernfalls wird beim Zugriff auf die Filme eines Superhelden eine Ausnahme ausgelöst. Wir möchten bei Bedarf auf die Filme eines Superhelden zugreifen können. In diesem Fall wählen wir die EAGER
Abrufstrategie.
CascadeType.PERSIST
Kaskadenoperationen definieren, wie Superhelden und ihre entsprechenden Filme in und aus der Datenbank gespeichert werden. Es stehen eine Reihe von Kaskadentypkonfigurationen zur Auswahl, über die wir später in diesem Lernprogramm mehr sprechen werden. Beachten Sie zunächst, dass wir das cascade
Attribut auf festgelegt CascadeType.PERSIST
haben. Wenn wir also einen Superhelden speichern, werden auch seine Filme gespeichert.
Tabellen verbinden
JoinTable
ist eine Klasse, die die Viele-zu-Viele-Beziehung zwischen SuperHero
und erleichtert Movie
. In dieser Klasse definieren wir die Tabelle, in der die Primärschlüssel sowohl für SuperHero
die Movie
Entitäten als auch für die Entitäten gespeichert werden .
Listing 1 gibt an, dass der Tabellenname lautet SuperHero_Movies
. Die Joinspalte wird superhero_id
, und die inverse Joinspalte sein wird movie_id
. Die SuperHero
Entität besitzt die Beziehung, sodass die Join-Spalte mit SuperHero
dem Primärschlüssel gefüllt wird. Die inverse Join-Spalte verweist dann auf die Entität auf der anderen Seite der Beziehung Movie
.
Basierend auf diesen Definitionen in Listing 1 würden wir erwarten, dass eine neue Tabelle mit dem Namen erstellt wird SuperHero_Movies
. Die Tabelle enthält zwei Spalten: superhero_id
die auf die id
Spalte der SUPERHERO
Tabelle movie_id
verweisen und die auf die id
Spalte der MOVIE
Tabelle verweisen .
Die Filmklasse
Listing 2 zeigt den Quellcode für die Movie
Klasse. Denken Sie daran, dass in einer bidirektionalen Beziehung eine Entität die Beziehung besitzt (in diesem Fall SuperHero
), während die andere der Beziehung zugeordnet ist. Der Code in Listing 2 enthält die auf die Movie
Klasse angewendete Beziehungszuordnung .
Listing 2. Movie.java
package com.geekcap.javaworld.jpa.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToMany; import javax.persistence.Table; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "MOVIE") public class Movie { @Id @GeneratedValue private Integer id; private String title; @ManyToMany(mappedBy = "movies", cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) private Set superHeroes = new HashSet(); public Movie() { } public Movie(Integer id, String title) { this.id = id; this.title = title; } public Movie(String title) { this.title = title; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Set getSuperHeroes() { return superHeroes; } public void addSuperHero(SuperHero superHero) { superHeroes.add(superHero); superHero.getMovies().add(this); } @Override public String toString() { return "Movie{" + "id=" + id + ", + title +"\'' + '}'; } }
Die folgenden Eigenschaften werden auf die @ManyToMany
Anmerkung in Listing 2 angewendet :
mappedBy
references the field name on theSuperHero
class that manages the many-to-many relationship. In this case, it references the movies field, which we defined in Listing 1 with the correspondingJoinTable
.cascade
is configured toCascadeType.PERSIST
, which means that when aMovie
is saved its correspondingSuperHero
entities should also be saved.fetch
tells theEntityManager
that it should retrieve a movie's superheroes eagerly: when it loads aMovie
, it should also load all correspondingSuperHero
entities.
Something else to note about the Movie
class is its addSuperHero()
method.
When configuring entities for persistence, it isn't enough to simply add a superhero to a movie; we also need to update the other side of the relationship. This means we need to add the movie to the superhero. When both sides of the relationship are configured properly, so that the movie has a reference to the superhero and the superhero has a reference to the movie, then the join table will also be properly populated.
We've defined our two entities. Now let's look at the repositories we'll use to persist them to and from the database.
Tip! Set both sides of the table
It's a common mistake to only set one side of the relationship, persist the entity, and then observe that the join table is empty. Setting both sides of the relationship will fix this.
JPA repositories
Wir könnten unseren gesamten Persistenzcode direkt in der Beispielanwendung implementieren, aber durch das Erstellen von Repository-Klassen können wir den Persistenzcode vom Anwendungscode trennen. Genau wie bei der Anwendung "Bücher und Autoren" in Teil 1 erstellen wir ein EntityManager
und verwenden es dann, um zwei Repositorys zu initialisieren, eines für jede Entität, die wir beibehalten.
Listing 3 zeigt den Quellcode für die MovieRepository
Klasse.
Listing 3. MovieRepository.java
package com.geekcap.javaworld.jpa.repository; import com.geekcap.javaworld.jpa.model.Movie; import javax.persistence.EntityManager; import java.util.List; import java.util.Optional; public class MovieRepository { private EntityManager entityManager; public MovieRepository(EntityManager entityManager) { this.entityManager = entityManager; } public Optional save(Movie movie) { try { entityManager.getTransaction().begin(); entityManager.persist(movie); entityManager.getTransaction().commit(); return Optional.of(movie); } catch (Exception e) { e.printStackTrace(); } return Optional.empty(); } public Optional findById(Integer id) { Movie movie = entityManager.find(Movie.class, id); return movie != null ? Optional.of(movie) : Optional.empty(); } public List findAll() { return entityManager.createQuery("from Movie").getResultList(); } public void deleteById(Integer id) { // Retrieve the movie with this ID Movie movie = entityManager.find(Movie.class, id); if (movie != null) { try { // Start a transaction because we're going to change the database entityManager.getTransaction().begin(); // Remove all references to this movie by superheroes movie.getSuperHeroes().forEach(superHero -> { superHero.getMovies().remove(movie); }); // Now remove the movie entityManager.remove(movie); // Commit the transaction entityManager.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); } } } }
Das MovieRepository
wird mit einem initialisiert EntityManager
und dann in einer Mitgliedsvariablen gespeichert, um es in seinen Persistenzmethoden zu verwenden. Wir werden jede dieser Methoden betrachten.
Persistenzmethoden
Lassen Sie uns MovieRepository
die Persistenzmethoden überprüfen und sehen, wie sie mit den EntityManager
Persistenzmethoden interagieren .