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 Movieund SuperHeroEntitä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 CascadeTypeStrategie 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
download Code herunterladen Laden Sie den Quellcode herunter, z. B. die in diesem Lernprogramm verwendeten Anwendungen. Erstellt von Steven Haines für JavaWorld.

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.

Steven Haines

Beachten Sie, dass dies SuperHero_Movieseine Verknüpfungstabelle zwischen den Tabellen Movieund 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 @ManyToManyAnnotation, 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 mappedByAttribut der @ManyToManyAnnotation, um diese Zuordnung zu erstellen.

Listing 1 zeigt den Quellcode für die SuperHeroKlasse.

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 SuperHeroKlasse enthält einige Anmerkungen, die aus Teil 1 bekannt sein sollten:

  • @Entityidentifiziert sich SuperHeroals JPA-Entität.
  • @Tableordnet die SuperHeroEntität der Tabelle "SUPER_HERO" zu.

Beachten Sie auch das IntegeridFeld, das angibt, dass der Primärschlüssel der Tabelle automatisch generiert wird.

Als nächstes schauen wir uns die @ManyToManyund @JoinTableAnmerkungen an.

Strategien abrufen

In der @ManyToManyAnmerkung ist zu beachten, wie wir die Abrufstrategie konfigurieren , die faul oder eifrig sein kann. In diesem Fall haben wir die Menge fetchzu EAGER, so dass , wenn wir ein Abrufen SuperHeroaus der Datenbank, werden wir auch alle seine entsprechenden automatisch abrufen Movies.

Wenn wir LAZYstattdessen einen Abruf durchführen würden, würden wir jeden nur abrufen, Moviewenn speziell darauf zugegriffen wurde. Faules Abrufen ist nur möglich, wenn das SuperHeroan 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 EAGERAbrufstrategie.

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 cascadeAttribut auf festgelegt CascadeType.PERSISThaben. Wenn wir also einen Superhelden speichern, werden auch seine Filme gespeichert.

Tabellen verbinden

JoinTableist eine Klasse, die die Viele-zu-Viele-Beziehung zwischen SuperHeround erleichtert Movie. In dieser Klasse definieren wir die Tabelle, in der die Primärschlüssel sowohl für SuperHerodie MovieEntitä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 SuperHeroEntität besitzt die Beziehung, sodass die Join-Spalte mit SuperHerodem 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_iddie auf die idSpalte der SUPERHEROTabelle movie_idverweisen und die auf die idSpalte der MOVIETabelle verweisen .

Die Filmklasse

Listing 2 zeigt den Quellcode für die MovieKlasse. 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 MovieKlasse 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 @ManyToManyAnmerkung in Listing 2 angewendet :

  • mappedBy references the field name on the SuperHero class that manages the many-to-many relationship. In this case, it references the movies field, which we defined in Listing 1 with the corresponding JoinTable.
  • cascade is configured to CascadeType.PERSIST, which means that when a Movie is saved its corresponding SuperHero entities should also be saved.
  • fetch tells the EntityManager that it should retrieve a movie's superheroes eagerly: when it loads a Movie, it should also load all corresponding SuperHero 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 EntityManagerund 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 MovieRepositoryKlasse.

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 MovieRepositorywird mit einem initialisiert EntityManagerund dann in einer Mitgliedsvariablen gespeichert, um es in seinen Persistenzmethoden zu verwenden. Wir werden jede dieser Methoden betrachten.

Persistenzmethoden

Lassen Sie uns MovieRepositorydie Persistenzmethoden überprüfen und sehen, wie sie mit den EntityManagerPersistenzmethoden interagieren .