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 Movie
y 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 CascadeType
estrategia 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
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.

Tenga en cuenta que SuperHero_Movies
es una tabla de combinación entre las tablas Movie
y 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 @ManyToMany
anotació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 mappedBy
atributo de la @ManyToMany
anotación para crear este mapeo.
El Listado 1 muestra el código fuente de la SuperHero
clase.
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 SuperHero
clase tiene un par de anotaciones que deberían resultarle familiares de la Parte 1:
@Entity
se identificaSuperHero
como una entidad JPA.@Table
asigna laSuperHero
entidad a la tabla "SUPER_HERO".
También tenga en cuenta el Integer
id
campo, que especifica que la clave principal de la tabla se generará automáticamente.
A continuación, veremos las anotaciones @ManyToMany
y @JoinTable
.
Buscando estrategias
Lo que hay que notar en la @ManyToMany
anotación es cómo configuramos la estrategia de búsqueda , que puede ser perezosa o ansiosa. En este caso, hemos configurado fetch
to EAGER
, de modo que cuando recuperemos un SuperHero
de la base de datos, también recuperemos automáticamente todos sus correspondientes correos electrónicos Movie
.
Si elegimos realizar una LAZY
recuperación en su lugar, solo recuperaríamos cada una de ellas Movie
cuando se haya accedido específicamente. La búsqueda diferida solo es posible mientras el SuperHero
está 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 EAGER
estrategia 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 cascade
atributo en CascadeType.PERSIST
, lo que significa que cuando guardemos un superhéroe, sus películas también se guardarán.
Unir tablas
JoinTable
es una clase que facilita la relación de varios a varios entre SuperHero
y Movie
. En esta clase, definimos la tabla que almacenará las claves primarias para SuperHero
las Movie
entidades y.
El Listado 1 especifica que el nombre de la tabla será SuperHero_Movies
. La columna de combinación será superhero_id
y la columna de combinación inversa será movie_id
. La SuperHero
entidad es propietaria de la relación, por lo que la columna de combinación se completará con SuperHero
la 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_id
que hace referencia a la id
columna de la SUPERHERO
tabla, y movie_id
, que hace referencia a la id
columna de la MOVIE
tabla.
La clase de película
El Listado 2 muestra el código fuente de la Movie
clase. 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 Movie
clase.
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 @ManyToMany
anotación en el Listado 2:
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
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 EntityManager
y luego lo usaremos para inicializar dos repositorios, uno para cada entidad que mantenemos.
El Listado 3 muestra el código fuente de la MovieRepository
clase.
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 MovieRepository
se 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 MovieRepository
métodos de persistencia y veamos cómo interactúan con los EntityManager
métodos de persistencia de.