Persistance Java avec JPA et Hibernate, partie 2: relations plusieurs-à-plusieurs

La première moitié de ce didacticiel a présenté les principes de base de l'API Java Persistence et vous a montré comment configurer une application JPA à l'aide d'Hibernate 5.3.6 et Java 8. Si vous avez lu ce didacticiel et étudié son exemple d'application, vous connaissez les bases de modéliser les entités JPA et les relations plusieurs-à-un dans JPA. Vous vous êtes également entraîné à écrire des requêtes nommées avec JPA Query Language (JPQL).

Dans cette seconde moitié du didacticiel, nous approfondirons JPA et Hibernate. Vous apprendrez à modéliser une relation plusieurs-à-plusieurs entre des entités Movieet SuperHero, à configurer des référentiels individuels pour ces entités et à conserver les entités dans la base de données en mémoire H2. Vous en apprendrez également plus sur le rôle des opérations en cascade dans JPA et obtiendrez des conseils pour choisir une CascadeTypestratégie pour les entités de la base de données. Enfin, nous allons créer une application fonctionnelle que vous pouvez exécuter dans votre IDE ou sur la ligne de commande.

Ce didacticiel se concentre sur les principes de base de JPA, mais assurez-vous de consulter ces conseils Java présentant des sujets plus avancés dans JPA:

  • Relations d'héritage dans JPA et Hibernate
  • Clés composites dans JPA et Hibernate
télécharger Obtenir le code Téléchargez le code source des exemples d'applications utilisées dans ce didacticiel. Créé par Steven Haines pour JavaWorld.

Relations plusieurs-à-plusieurs dans JPA

Les relations plusieurs à plusieurs définissent des entités pour lesquelles les deux côtés de la relation peuvent avoir plusieurs références l'une à l'autre. Pour notre exemple, nous allons modéliser des films et des super-héros. Contrairement à l'exemple Auteurs et livres de la partie 1, un film peut avoir plusieurs super-héros et un super-héros peut apparaître dans plusieurs films. Nos super-héros, Ironman et Thor, apparaissent tous deux dans deux films, "The Avengers" et "Avengers: Infinity War".

Pour modéliser cette relation plusieurs-à-plusieurs à l'aide de JPA, nous aurons besoin de trois tables:

  • FILM
  • SUPER_HERO
  • SUPERHERO_MOVIES

La figure 1 montre le modèle de domaine avec les trois tableaux.

Steven Haines

Notez qu'il SuperHero_Moviess'agit d'une table de jointure entre les tables Movieet SuperHero. Dans JPA, une table de jointure est un type spécial de table qui facilite la relation plusieurs-à-plusieurs.

Unidirectionnel ou bidirectionnel?

Dans JPA, nous utilisons l' @ManyToManyannotation pour modéliser les relations plusieurs-à-plusieurs. Ce type de relation peut être unidirectionnel ou bidirectionnel:

  • Dans une relation unidirectionnelle, une seule entité de la relation pointe vers l'autre.
  • Dans une relation bidirectionnelle, les deux entités se pointent l'une vers l'autre.

Notre exemple est bidirectionnel, ce qui signifie qu'un film pointe vers tous ses super-héros et un super-héros pointe vers tous leurs films. Dans une relation plusieurs-à-plusieurs bidirectionnelle, une entité possède la relation et l'autre est mappée à la relation. Nous utilisons l' mappedByattribut de l' @ManyToManyannotation pour créer ce mappage.

Le listing 1 montre le code source de la SuperHeroclasse.

Liste 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 a quelques annotations qui devraient être familières de la partie 1:

  • @Entitys'identifie SuperHerocomme une entité JPA.
  • @Tablemappe l' SuperHeroentité à la table "SUPER_HERO".

Notez également le Integeridchamp, qui spécifie que la clé primaire de la table sera générée automatiquement.

Ensuite, nous examinerons les annotations @ManyToManyet @JoinTable.

Récupération de stratégies

La chose à noter dans l' @ManyToManyannotation est la façon dont nous configurons la stratégie de récupération , qui peut être paresseuse ou impatiente. Dans ce cas, nous avons défini le fetchsur EAGER, de sorte que lorsque nous récupérons un SuperHerode la base de données, nous récupérons également automatiquement tous ses Movies correspondants .

Si nous choisissons d'effectuer une LAZYrécupération à la place, nous ne récupérerons chacun Movied' eux que lorsqu'ils ont été spécifiquement accédés. La récupération paresseuse n'est possible que lorsque le SuperHeroest attaché au EntityManager; sinon, accéder aux films d'un super-héros lèvera une exception. Nous voulons pouvoir accéder aux films d'un super-héros à la demande, donc dans ce cas, nous choisissons la EAGERstratégie de récupération.

CascadeType.PERSIST

Les opérations en cascade définissent comment les super-héros et leurs films correspondants sont conservés vers et depuis la base de données. Il existe un certain nombre de configurations de type en cascade parmi lesquelles choisir, et nous en reparlerons plus tard dans ce didacticiel. Pour l'instant, notez simplement que nous avons défini l' cascadeattribut sur CascadeType.PERSIST, ce qui signifie que lorsque nous sauvegardons un super-héros, ses films seront également enregistrés.

Joindre des tables

JoinTableest une classe qui facilite la relation plusieurs-à-plusieurs entre SuperHeroet Movie. Dans cette classe, nous définissons la table qui stockera les clés primaires pour SuperHeroles Movieentités et.

Le listing 1 spécifie que le nom de la table sera SuperHero_Movies. La colonne de jointure sera superhero_id, et la colonne de jointure inverse sera movie_id. L' SuperHeroentité est propriétaire de la relation, donc la colonne de jointure sera remplie avec SuperHerola clé primaire de. La colonne de jointure inverse fait alors référence à l'entité de l'autre côté de la relation, qui est Movie.

Sur la base de ces définitions dans le listing 1, nous nous attendrions à ce qu'une nouvelle table soit créée, nommée SuperHero_Movies. Le tableau aura deux colonnes:, superhero_idqui fait référence à la idcolonne du SUPERHEROtableau, et movie_id, qui fait référence à la idcolonne du MOVIEtableau.

La classe de cinéma

Le listing 2 montre le code source de la Movieclasse. Rappelez-vous que dans une relation bidirectionnelle, une entité possède la relation (dans ce cas SuperHero) tandis que l'autre est mappée à la relation. Le code du Listing 2 inclut le mappage de relation appliqué à la Movieclasse.

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

Les propriétés suivantes sont appliquées à l' @ManyToManyannotation dans le listing 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

Nous pourrions implémenter tout notre code de persistance directement dans l'exemple d'application, mais la création de classes de référentiel nous permet de séparer le code de persistance du code d'application. Tout comme nous l'avons fait avec l'application Books & Authors dans la partie 1, nous allons créer un fichier EntityManager, puis l'utiliser pour initialiser deux référentiels, un pour chaque entité que nous conservons.

Le listing 3 montre le code source de la MovieRepositoryclasse.

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

Le MovieRepositoryest initialisé avec un EntityManager, puis l'enregistre dans une variable membre à utiliser dans ses méthodes de persistance. Nous examinerons chacune de ces méthodes.

Méthodes de persistance

Passons en revue MovieRepositoryles méthodes de persistance de et voyons comment elles interagissent avec les EntityManagerméthodes de persistance de.