Persistencia de Java con JPA e Hibernate, Parte 2: Relaciones de varios a varios

La primera mitad de este tutorial presentó los fundamentos de la API de persistencia de Java y le mostró cómo configurar una aplicación JPA usando Hibernate 5.3.6 y Java 8. Si ha leído ese tutorial y estudiado su aplicación de ejemplo, entonces conoce los fundamentos de Modelado de entidades JPA y relaciones de muchos a uno en JPA. También ha tenido algo de práctica escribiendo consultas con nombre con JPA Query Language (JPQL).

En esta segunda mitad del tutorial profundizaremos con JPA e Hibernate. Aprenderá a modelar una relación de varios a varios entre entidades Moviey SuperHero, configurar repositorios individuales para estas entidades y conservar las entidades en la base de datos H2 en memoria. También aprenderá más sobre la función de las operaciones en cascada en JPA y obtendrá sugerencias para elegir una CascadeTypeestrategia para las entidades en la base de datos. Finalmente, reuniremos una aplicación funcional que puede ejecutar en su IDE o en la línea de comandos.

Este tutorial se centra en los fundamentos de JPA, pero asegúrese de consultar estos consejos de Java que presentan temas más avanzados en JPA:

  • Relaciones de herencia en JPA e Hibernate
  • Claves compuestas en JPA e Hibernate
descargar Obtener el código Descargar el código fuente, por ejemplo, las aplicaciones utilizadas en este tutorial. Creado por Steven Haines para JavaWorld.

Relaciones de muchos a muchos en JPA

Las relaciones de varios a varios definen entidades para las que ambos lados de la relación pueden tener múltiples referencias entre sí. Para nuestro ejemplo, vamos a modelar películas y superhéroes. A diferencia del ejemplo de Autores y libros de la Parte 1, una película puede tener varios superhéroes y un superhéroe puede aparecer en varias películas. Nuestros superhéroes, Ironman y Thor, aparecen en dos películas, "Los Vengadores" y "Vengadores: Infinity War".

Para modelar esta relación de muchos a muchos usando JPA, necesitaremos tres tablas:

  • PELÍCULA
  • SUPER_HÉROE
  • SUPERHERO_MOVIES

La Figura 1 muestra el modelo de dominio con las tres tablas.

Steven Haines

Tenga en cuenta que SuperHero_Movieses una tabla de combinación entre las tablas Moviey SuperHero. En JPA, una tabla de combinación es un tipo especial de tabla que facilita la relación de varios a varios.

¿Unidireccional o bidireccional?

En JPA usamos la @ManyToManyanotación para modelar relaciones de varios a varios. Este tipo de relación puede ser unidireccional o bidireccional:

  • En una relación unidireccional, solo una entidad de la relación señala a la otra.
  • En una relación bidireccional, ambas entidades se apuntan entre sí.

Nuestro ejemplo es bidireccional, lo que significa que una película apunta a todos sus superhéroes y un superhéroe apunta a todas sus películas. En una relación bidireccional de varios a varios, una entidad es propietaria de la relación y la otra se asigna a la relación. Usamos el mappedByatributo de la @ManyToManyanotación para crear este mapeo.

El Listado 1 muestra el código fuente de la SuperHeroclase.

Listado 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 SuperHeroclase tiene un par de anotaciones que deberían resultarle familiares de la Parte 1:

  • @Entityse identifica SuperHerocomo una entidad JPA.
  • @Tableasigna la SuperHeroentidad a la tabla "SUPER_HERO".

También tenga en cuenta el Integeridcampo, que especifica que la clave principal de la tabla se generará automáticamente.

A continuación, veremos las anotaciones @ManyToManyy @JoinTable.

Buscando estrategias

Lo que hay que notar en la @ManyToManyanotación es cómo configuramos la estrategia de búsqueda , que puede ser perezosa o ansiosa. En este caso, hemos configurado fetchto EAGER, de modo que cuando recuperemos un SuperHerode la base de datos, también recuperemos automáticamente todos sus correspondientes correos electrónicos Movie.

Si elegimos realizar una LAZYrecuperación en su lugar, solo recuperaríamos cada una de ellas Moviecuando se haya accedido específicamente. La búsqueda diferida solo es posible mientras el SuperHeroestá adjunto al EntityManager; de lo contrario, acceder a las películas de un superhéroe generará una excepción. Queremos poder acceder a las películas de un superhéroe bajo demanda, así que en este caso elegimos la EAGERestrategia de búsqueda.

CascadeType.PERSIST

Las operaciones en cascada definen cómo los superhéroes y sus películas correspondientes se conservan desde y hacia la base de datos. Hay varias configuraciones de tipo cascada para elegir, y hablaremos más sobre ellas más adelante en este tutorial. Por ahora, solo tenga en cuenta que hemos establecido el cascadeatributo en CascadeType.PERSIST, lo que significa que cuando guardemos un superhéroe, sus películas también se guardarán.

Unir tablas

JoinTablees una clase que facilita la relación de varios a varios entre SuperHeroy Movie. En esta clase, definimos la tabla que almacenará las claves primarias para SuperHerolas Movieentidades y.

El Listado 1 especifica que el nombre de la tabla será SuperHero_Movies. La columna de combinación será superhero_idy la columna de combinación inversa será movie_id. La SuperHeroentidad es propietaria de la relación, por lo que la columna de combinación se completará con SuperHerola clave principal. La columna de combinación inversa luego hace referencia a la entidad en el otro lado de la relación, que es Movie.

Basado en estas definiciones en el Listado 1, esperaríamos crear una nueva tabla, nombrada SuperHero_Movies. La tabla tendrá dos columnas:, superhero_idque hace referencia a la idcolumna de la SUPERHEROtabla, y movie_id, que hace referencia a la idcolumna de la MOVIEtabla.

La clase de película

El Listado 2 muestra el código fuente de la Movieclase. Recuerde que en una relación bidireccional, una entidad es propietaria de la relación (en este caso SuperHero) , mientras que la otra se asigna a la relación. El código del Listado 2 incluye el mapeo de relaciones aplicado a la Movieclase.

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

Las siguientes propiedades se aplican a la @ManyToManyanotación en el Listado 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

Podríamos implementar todo nuestro código de persistencia directamente en la aplicación de muestra, pero la creación de clases de repositorio nos permite separar el código de persistencia del código de la aplicación. Al igual que hicimos con la aplicación Libros y Autores en la Parte 1, crearemos un archivo EntityManagery luego lo usaremos para inicializar dos repositorios, uno para cada entidad que mantenemos.

El Listado 3 muestra el código fuente de la MovieRepositoryclase.

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

El MovieRepositoryse inicializa con an EntityManager, luego lo guarda en una variable miembro para usarlo en sus métodos de persistencia. Consideraremos cada uno de estos métodos.

Métodos de persistencia

Repasemos los MovieRepositorymétodos de persistencia y veamos cómo interactúan con los EntityManagermétodos de persistencia de.