Relaciónes @ManyToMany

Las relaciones Mucho a Muchos (@ManyToMany) se caracterízan por Entidades que están relacionadas con a muchos elementos de un tipo determinado, pero al mismo tiempo, estos últimos registros no son exclusivos de un registro en particular, si no que pueden ser parte de varios, por lo tanto, tenemos una Entidad A, la cual puede estar relacionada como muchos registros de la Entidad B, pero al mismo tiempo, la Entidad B puede pertenecer a varias instancias de la Entidad A.

Algo muy importante a tomar en cuenta cuando trabajamos con relaciones @ManyToMany, es que en realidad este tipo de relaciones no existen físicamente en la base de datos, y en su lugar, es necesario crear una tabla intermedia que relaciones las dos Entidades, veremos más adelante como resolvemos eso.

Un ejemplo clásico de estas relaciones son los libros con sus autores, de esta forma, un libro puede tener varios autores, y a su vez, los autores puede tener muchos libros. Pero para que quede más claro, veamos como quedarían las Entidades de Autor (Author), Libro (Book):


Entidad Book:

package com.oscarblancarteblog;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.*;

@Entity
@Table(name = "books")
public class Book {
    @Id
    @Column(name="ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "NAME", nullable = false)
    private String name;
    
    @JoinTable(
        name = "rel_books_auths",
        joinColumns = @JoinColumn(name = "FK_BOOK", nullable = false),
        inverseJoinColumns = @JoinColumn(name="FK_AUTHOR", nullable = false)
    )
    @ManyToMany(cascade = CascadeType.ALL)
    private List<Author> authors;
   
    public void addAuthor(Author author){
        if(this.authors == null){
            this.authors = new ArrayList<>();
        }
        
        this.authors.add(author);
    }

    /** GET and SET */
  
}

Como podemos apreciar, hemos creado una lista de tipo Author, la cual es anotada con @ManyToMany, adicional, hemos definido la anotación @JoinTable, la cual nos sirve para definir la estructura de la tabla intermedia que contendrá la relación entre los libros y los autores.

La anotación @JoinTable no es obligatoria en sí, ya que en caso de no definirse JPA asumirá el nombre de la tabla, columnas, longitud, etc. Para no quedar a merced de la implementación de JPA, siempre es recomendable definirla, así, tenemos el control total sobre ella.

Hemos definidos las siguientes propiedades de la anotación @JoinTable:

  • name: Nombre de la tabla que será creada físicamente en la base de datos.
  • joinColumns: Corresponde al nombre para el ID de la Entidad Book.
  • inverseJoinColumns: Corresponde al nombre para el ID de la Entidad Author


Entidad Author:

package com.oscarblancarteblog;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.*;

@Entity
@Table(name="authors")
public class Author {
    
    @Id
    @Column(name="ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name="NAME", nullable = false)
    private String name;
    
    @ManyToMany(mappedBy = "authors")
    private List<Book> books;

    /** GET and SET **/
}

El caso de la Entidad Author es más simple, pues solo marcamos la colección con @ManyToMany, pero en este caso ya no es necesario definir la anotación @JoinTable, en su lugar, definimos la propiedad mappedBy para indicar la relación bidireccional y al mismo tiempo, JPA puede tomar la configuración del @JoinTable de Books.

Como resultado de estas Entidades, tendremos las siguientes tablas auto generadas:

Notemos en la tabla authors no tiene una columna que haga referencia a books, ni books a authors, si no que es necesario tener una tabla intermedia que haga el cruce entre las dos tablas.

La tabla intermedia (rel_book_auths) es generada por la anotación @JoinTable y sus dos columnas son llaves foraneas a las tablas books y authors.


Prueba de validación

Para comprobar que todo funciona como lo hemos dicho, vamos a realizar una prueba, la cual se ve de la siguiente manera:

public static void main(String[] args) {
        
	//Authors
	Author author1 = new Author();
	author1.setName("Juan Perez");
	
	Author author2 = new Author();
	author2.setName("Oscar Blancarte");
	
	Author author3 = new Author();
	author3.setName("Arturo Martinez");
	
	
	//Books
	Book book1 = new Book();
	book1.setName("El lago y el pato");
	book1.addAuthor(author1);
	book1.addAuthor(author2);
	book1.addAuthor(author3);
	
	Book book2 = new Book();
	book2.setName("Una mañana de verano");
	book2.addAuthor(author1);
	book2.addAuthor(author2);
	book2.addAuthor(author3);
	
	EntityManager em = EntityManagerUtil.getEntityManager();
	em.getTransaction().begin();
	em.persist(book1);
	em.persist(book2);
	em.getTransaction().commit();
	
	System.out.println("FIN");
}

Hemos creados dos libros y tres autores, y luego hemos asociado a los autores a los libros, con la intención de que los autores estén en dos libros y los libros tengan varios autores.

Tambíen los quiero invitar a ver mi curso de JPA, donde explico todos estos temas aplicados con API REST, https://codmind.com/courses/jpa

 Los invito a mi Curso de Mastering JPA, donde habla de todos estos temas y crearemos un API REST para probar todos los conceptos de persistencia.

Ahora veamos como se ven las tablas authors, books y rel_books_auths:

Para poder obtener la relación entre libros y autores solo faltaría hacer la unión entre las dos tablas.

Conclusiones

Para concluir solo faltaría resaltar que en las relaciones @ManyToMany los registros son independientes de los registros a los que son relacionados, por lo que en este caso, podrían existir los autores si no existieran los libros, y al revés.

AnteriorÍndiceSiguiente

35 thoughts to “Relaciónes @ManyToMany”

  1. Estimado Oscar:
    Tengo una duda. Segun entiendo, con la configuracion de relacion que escribes arriba, si al intentar guardar un libro con el mismo autor, meteria en la tabla AUTHORS un nuevo registro con el mismso nombre del author pero con un id distinto obviamente. Entonces en esta tabla tendriamos el mismo author tantas veces hayamos agregado un libro con ese author, pero con ids distintos.
    En el caso de querer persistir un nuevo libro pero con author ya existente en la BD sin que se sobre-escriba o duplique con id diferente, como se tendria que hacer.
    Mas aun, si tomaras un author existen y se lo metes al nuevo libro, lanzaria una excepcion tipo “com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry ‘id_de la_tupla_author’ for key ‘PRIMARY'”.
    Lo q estaria diciendo, logicamente, que no puede registrar una nueva tupla porque ya se encuentra grabada y la primary key se entiende univoca.

    1. Hola Aldo, lo que tienes que haces es hacer un find del Autor y luego asignarlo a los libros, de esta forma el EntityManager sabrá que se trata de un Autor existente y no lo persistirá, si no que solo tomará el ID para asignarlo al libro.

  2. Hola Oscar, me puedes aclarar un poco esto, porque si estoy un poco perdida con el asunto, acá dices: “Para poder obtener la relación entre libros y autores solo faltaría hacer la unión entre las dos tablas.”, para hacer esto debería tener una tabla entidad que se llame como la relación que creamos? sino es así de que forma pudiera hacerlo.

    Gracias

    1. Hola Yas, la cuestión es muy simple, en las bases de datos no existen las relaciones ManyToMany, en su lugar, es necesario crear una tabla intermedia que relaciones a los dos entendidas, en realidad no existe otra alternativa, tienes que crear una tabla intermedia si quieres implementar ManyToMany

  3. son 3 tablas que estan relacionadas . Las tablas son productos, pedido y sucursal. Existe una relación entre el pedido y producto. La cuestión es que cada producto del pedido puede tener diferente sucursal.
    Por ende pienso en una relación en la cual la tabla pedido, producto, sucursal se relacionen con una tabla llamada pedido_producto la cual contenga foraneas de las tres tablas anteriores.
    En el ejemplo que muestras, indicas como manejar una relacion de muchos a muchos. Quisiera saber si de esta manera puedo generar una referencia sobre otro entity para que me cree la relación que requiero

    Gracias 😀

    1. Yo lo que haría sería crear una relación de @OneToMany del pedido al producto y del producto a la sucursal sería @ManyToOne, aun que no conozco bien tu escenario de negocio, quizás este equivocado,

  4. Hola, gracias por la explicacion. Queria preguntarte en el caso de querer elimnar un book como seria el proceso, he estado intentando hacer algo parecido y me da un error de restriccion en la fk. Gracias !

    1. El problema se puede deber que al borrar el Book, este tiene una relación con el Autor y una cascada CascadeType.ALL, lo que significa que cuando borras el Book, se intenta borrar en cascada el Autor, lo que puede provocar que otros Book este relacionado con el Autor que intentas eliminar, lo que podrás hacer es cambiar el CascadeType a solo PERSIST.
      saludos,

  5. Funciona si creas un libro y le añades autores y luego guardas el libro, entonces persisten ambas entidades y su relación en la tabla one-to-many/many-to-one llamada rel_books_auths.

    Sin embarga la relación inversa es una situación también válida, crear un autor y añadirle libros, creando un método addBook en la clase Author, en ese caso si guardas el autor con los libros se crea el autor pero no los libros y por lo tanto se pierde tanto los libros como la relación.

    ¿Cómo se puede solucionar esto con @ManyToMany?, ¿o no se puede?

    1. Esto se debe a la cascada, observa que en la clase Book la relación con los autores tiene una cascada de tipo `CascadeType.ALL`, eso provoca que cuando guardar el libro se guarda la relación con los autores, para hacer lo que tu dices tendrías que definir la casca desde el Autor.
      saludos.

  6. Hola Oscar, cómo se haría eso en un RestController con Spring Boot?
    ¿Cómo hago para enviar un Json del libro a crear, donde vayan anidados los autores y autor que no exista se cree, sin conocer todavía el id del libro al que pertenece?
    Osea digamos que yo tengo un formulario con Angular para crear libros y en el formulario esté la opción para añadir autores y si el autor no existe haya un botón donde se pueda agregar uno nuevo, y al final enviar todo en un json al backend.

    1. Hola Wilbert, tu pregunta es compleja por que abarca varias cosas, pero te podría decir que deberías de crear un servicio que acepte un DTO, este DTO deberá aceptar todo lo que tienes en la pantalla, incluido el libro y los Autores, el primer paso de tu servicio deberá ser crear los autores que no existan, luego crear el libro, de esta forma, ya abras creado los autores antes de crear el libro.
      Te aconsejo que veas nuestro curso de Desarrollo de microservicios con Spring Boot donde explicamos todo esto

  7. Hola oscar

    tengo una cuestion, tengo 3 tablas: grupos, investigadores y solicitudes
    la cuestion es, entre grupos e investigadores va n*n, entonces creé la otra tabla esa que son las dos llaves foraneas(llamemosla cruze)
    la cuestión es que la tabla solicitudes se relaciona 1 a 1 con la tabla cruze(la que relaciona las otras dos)
    entonces le asigné una primary key a la tabla cruze, para despues en solicitud, realizar la relacion 1 a 1 con ésta
    pero entonces en java creo una entidad cruze
    y veo en todos los ejemplos que encuentro, que eso no se codifica, no se crea esta entidad, pero claro, porque no hay otra tercera tabla, está bien lo que pienso hacer? o cómo lo haría?
    o se tiene que hacer esa relación sin tener que crear una entidad cruze?

    1. Lo que tienes que haces es usar la anotación @JoinTable en tu relación @ManyToMany. Esta anotación permite definir la estructura de esta tabla de “crece” definir el nombre de las columnas y el nombre de la tabla, de esta forma, JPA se encarga de administrar sin la necesidad de tener que crear una Entity para gestionar la tabla de “cruce”.

      1. pero eso con las dos tablas para que no salga cruce, que es como están los ejemplos

        pero, cómo haría para relacionar esas dos con solicitud?
        supongamos que hago lo que me dices en la tabla de investigadores, y coloco los jointable y manytomany

        ¿en solicitud hago relacion onebyone hacía investigadores?

  8. Hola Oscar.

    Muchas gracias por el Post, muy claro. Tengo una duda, aunque no sé si esta entrada es la más oportuna. Cuando intento mostrar esto, obtengo un bucle infinito, y no se me muestra. ¿Cómo puedo conseguir esto?
    – cuando pido los libros, me saque todos los atributos de los libros pero sólo me muestre el id y el nombre de los autores
    – cuando pido los autores, me saque todos los atributos de los autores, pero sólo me muestre el id y el nombre de los libros

    Muchas gracias de antemano

    1. Hola Lisco, de casualidad estás retornando las Entidades por medio de un EJB, Webservice, Rest? por que esto suele pasar cuando se serializan las entidades para viajar por la red. en tal caso, debes evitar las relaciones cíclicas mediante el patrón DTO

  9. hola!! quisera comentarte lo que me sucedio con la relacion oneToMany, tengo dos entidades
    habitacion y tipoHabitacion, cuando utilizo el metodo finById(id) en @GetMapping me generaba un bucle infinito, entoces probe haciendo la relacion unidireccional (solo mapeada desde el lado de “habitacion y funciono”, pero queria que la relacion fuera bidireccional o al menos saber como mapear bien la relacion bidireccional, para hacer esto lei en stackoverflow que puede generar este tipo de problemas el hecho de que los id de cada tabla se llamen igual (ya los cambie para probar) y evidentemente esa era el problema porque funciono perfecto!! pero me pregunto…si yo he visto por todo lados que utilizan el mismo nombre “id” en las dos clases y no tiene problemas!! quisiera saber el porque a mi no me funciona con el mismo nombre? te agradeceria muchisimo si me lo explicas.

    aclaro por las dudas, no se si tendra relacion pero uso la libreria de Lombok (@Data @AllArgsConstructor @NoArgsConstructor) porque tambien lei que puede ser el metodo toString() el que puede ocasionar este tipo de errores

    estoy recien empezando con spring boot, y con este problema tambien me planteo la duda de cuando es conveniente usar relacion bidireccional o unidireccional

    public class Habitacion implements Serializable{

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO, generator=”native”)
    @GenericGenerator(name=”native”,strategy=”native”)
    @Column
    private long id_habitacion;

    @Column
    @NotBlank
    private String numerohabitacion;

    /*union con Tipos de habitacion*/
    @ManyToOne
    @JoinColumn(name=”id_tipohabitacion”)
    private TipoHabitacion tipoHabitacion;
    }
    public class TipoHabitacion implements Serializable{
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO, generator=”native”)
    @GenericGenerator(name=”native”,strategy=”native”)
    @Column
    private long id_tipo;

    @Column
    @NotBlank
    private String clase;

    @OneToMany(cascade = CascadeType.ALL)
    private Set habitacion;
    }

    1. Hola Lorena,
      Creo que el problema es por que no estas mapenado adecuadamente la Entidad, faltaría que agregaras la propiedad mappedBy a la anotación @OneToMany de la clase TipoHabitacion para que quede de la siguiente forma @OneToMany(mappedBy="tipoHabitacion").
      Intenta eso y me dices como te fue.

  10. Hola Julio, antes de todo quiro darte las gracias por todo el material que me ha ayudado a crecer. Mi duda es la siguiente, si ya tengo una DB con relaciones, es obligatorio en las Entity realizar las relaciones o se puedene manejar desde Spring Boot como tablas separadas?.

    Desde ya Muchisimas gracias.

    1. Hola Karina, lo mas recomendable es que las Entidades cumplan con las relaciones que ya tiene tu base de datos, de lo contrario, podrías tener muchos problemas al persistir, actualizar o borrar, ya que JPA utiliza las relaciones para optimizar lo Query, así como las operaciones CRUD.
      saludos.

  11. Hola Oscar! Muy útil la información. Una pregunta, en la tabla de relación se puede añadir un campo más? Una fecha por ejemplo. Es posible o se debe agregar otra tabla? Gracias de antemano

    1. Siempre que sean parte de llave es posible, solo basta agregar más anotaciones @JoinColum o @InverserJoinColumns dentro de las anotaciones @JoinColumns (observa que termina con S)

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *