Persistenza Java con JPA e Hibernate, Parte 2: relazioni molti-a-molti

La prima metà di questo tutorial ha introdotto i fondamenti dell'API Java Persistence e ti ha mostrato come configurare un'applicazione JPA usando Hibernate 5.3.6 e Java 8. Se hai letto quel tutorial e studiato la sua applicazione di esempio, allora conosci le basi di modellazione di entità JPA e relazioni molti-a-uno in JPA. Hai anche esercitato la scrittura di query con nome con JPA Query Language (JPQL).

In questa seconda metà del tutorial andremo più a fondo con JPA e Hibernate. Imparerai come modellare una relazione molti-a-molti tra le entità Moviee SuperHero, impostare singoli repository per queste entità e rendere persistenti le entità nel database in memoria H2. Imparerai anche di più sul ruolo delle operazioni a cascata in JPA e riceverai suggerimenti per la scelta di una CascadeTypestrategia per le entità nel database. Infine, metteremo insieme un'applicazione funzionante che puoi eseguire nel tuo IDE o sulla riga di comando.

Questo tutorial si concentra sui fondamenti di JPA, ma assicurati di dare un'occhiata a questi suggerimenti Java che introducono argomenti più avanzati in JPA:

  • Relazioni di eredità in JPA e Hibernate
  • Chiavi composite in JPA e Hibernate
download Ottieni il codice Scarica il codice sorgente per le applicazioni di esempio utilizzate in questo tutorial. Creato da Steven Haines per JavaWorld.

Relazioni molti-a-molti nell'APP

Le relazioni molti-a-molti definiscono entità per le quali entrambi i lati della relazione possono avere più riferimenti l'uno all'altro. Per il nostro esempio, modelleremo film e supereroi. A differenza dell'esempio Autori e libri della Parte 1, un film può avere più supereroi e un supereroe può apparire in più film. I nostri supereroi, Ironman e Thor, appaiono entrambi in due film, "The Avengers" e "Avengers: Infinity War".

Per modellare questa relazione molti-a-molti utilizzando JPA, avremo bisogno di tre tabelle:

  • FILM
  • SUPEREROE
  • SUPERHERO_MOVIES

La figura 1 mostra il modello di dominio con le tre tabelle.

Steven Haines

Notare che SuperHero_Moviesè una tabella di join tra le tabelle Moviee SuperHero. In JPA, una tabella di join è un tipo speciale di tabella che facilita la relazione molti-a-molti.

Unidirezionale o bidirezionale?

In JPA usiamo l' @ManyToManyannotazione per modellare le relazioni molti-a-molti. Questo tipo di relazione può essere unidirezionale o bidirezionale:

  • In una relazione unidirezionale solo un'entità nella relazione punta l'altra.
  • In una relazione bidirezionale entrambe le entità puntano l'una verso l'altra.

Il nostro esempio è bidirezionale, il che significa che un film punta a tutti i suoi supereroi e un supereroe punta a tutti i suoi film. In una relazione bidirezionale molti-a-molti, un'entità possiede la relazione e l'altra è mappata alla relazione. Usiamo l' mappedByattributo @ManyToManydell'annotazione per creare questa mappatura.

Il listato 1 mostra il codice sorgente per la SuperHeroclasse.

Listato 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()) +"\'' + '}'; } } 

La SuperHeroclasse ha un paio di annotazioni che dovrebbero essere familiari dalla Parte 1:

  • @Entitysi identifica SuperHerocome un'entità JPA.
  • @Tablemappa l' SuperHeroentità alla tabella "SUPER_HERO".

Notare anche il Integeridcampo, che specifica che la chiave primaria della tabella verrà generata automaticamente.

Successivamente esamineremo le annotazioni @ManyToManye @JoinTable.

Recupero di strategie

La cosa da notare nell'annotazione @ManyToManyè come configuriamo la strategia di recupero , che può essere pigra o desiderosa. In questo caso, abbiamo impostato fetchsu EAGER, in modo che quando recuperiamo un SuperHerodal database, recupereremo automaticamente anche tutti i suoi corrispondenti Movie.

Se invece scegliamo di eseguire un LAZYrecupero, recupereremo solo ciascuno Moviepoiché è stato specificamente acceduto. Il recupero pigro è possibile solo mentre SuperHeroè collegato a EntityManager; altrimenti l'accesso ai film di un supereroe genererà un'eccezione. Vogliamo poter accedere ai film di un supereroe on demand, quindi in questo caso scegliamo la EAGERstrategia di recupero.

CascadeType.PERSIST

Le operazioni a cascata definiscono il modo in cui i supereroi ei loro film corrispondenti vengono mantenuti da e verso il database. Esistono numerose configurazioni di tipo a cascata tra cui scegliere e ne parleremo più avanti in questo tutorial. Per ora, tieni presente che abbiamo impostato l' cascadeattributo su CascadeType.PERSIST, il che significa che quando salviamo un supereroe, verranno salvati anche i suoi film.

Partecipa ai tavoli

JoinTableè una classe che facilita la relazione molti-a-molti tra SuperHeroe Movie. In questa classe, definiamo la tabella che memorizzerà le chiavi primarie sia per SuperHerole Movieentità che per.

Il listato 1 specifica che il nome della tabella sarà SuperHero_Movies. La colonna di join sarà superhero_ide la colonna di join inversa sarà movie_id. L' SuperHeroentità è proprietaria della relazione, quindi la colonna join verrà popolata con SuperHerola chiave primaria di. La colonna del join inverso fa quindi riferimento all'entità sull'altro lato della relazione, ovvero Movie.

Sulla base di queste definizioni nel Listato 1, ci aspetteremmo che venga creata una nuova tabella, denominata SuperHero_Movies. La tabella avrà due colonne:, superhero_idche fa riferimento alla idcolonna della SUPERHEROtabella e movie_id, che fa riferimento alla idcolonna della MOVIEtabella.

La classe di film

Il Listato 2 mostra il codice sorgente per la Movieclasse. Ricorda che in una relazione bidirezionale, un'entità possiede la relazione (in questo caso SuperHero) mentre l'altra è mappata alla relazione. Il codice nel listato 2 include la mappatura delle relazioni applicata alla Movieclasse.

Listato 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 +"\'' + '}'; } }

Le seguenti proprietà vengono applicate @ManyToManyall'annotazione nel Listato 2:

  • 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

Potremmo implementare tutto il nostro codice di persistenza direttamente nell'applicazione di esempio, ma la creazione di classi di repository ci consente di separare il codice di persistenza dal codice dell'applicazione. Proprio come abbiamo fatto con l'applicazione Libri e autori nella Parte 1, creeremo un EntityManagere poi lo useremo per inizializzare due repository, uno per ogni entità che stiamo persistendo.

Il Listato 3 mostra il codice sorgente per la MovieRepositoryclasse.

Listato 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(); } } } } 

La MovieRepositoryviene inizializzato con una EntityManager, poi salva in una variabile membro da utilizzare nei metodi di persistenza. Considereremo ciascuno di questi metodi.

Metodi di persistenza

Rivediamo MovieRepositoryi metodi di persistenza di e vediamo come interagiscono con EntityManageri metodi di persistenza di.