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.

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

16 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.

Deja un comentario

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