Ejemplo de relación ManyToMany en Hibernate
En esta entrada vamos a mostrar un ejemplo de relación ManyToMany en hibernate, con la que veremos como mapear y hacer uso de JPA a través de Hibernate para conseguir este tipo de relaciones.
Si necesitas ver como realizar una relación OneToMany puedes echar un ojo aquí.
En el artículo veremos también cual es la manera más eficiente de realizar este tipo de relaciones haciendo uso de la anotación @ManyToMany.
¿Cómo funciona una relación ManyToMany?
Una relación ManyToMany es aquella en la que se va a generar una tabla intermedia. Por ejemplo, tenemos dos entidades, trabajador y departamento. Un departamento tendrá 1..N trabajadores y un trabajador podrá estar en 1..M departamentos, por lo que tenemos una relación M..N, con una tabla extra. Esta tabla extra se representará como una relación 1..N:
Esta sería la representación en Base de Datos de una relación N..M, tendríamos las dos tablas origen y una nueva que se forma a partir de las otras dos. Esta nueva tabla tendrá los id’s referenciando a las otras dos tablas.
En función de como implementemos esta relación podemos obtener diferentes resultados, en este artículo vamos a ver la implementación de la relación ManyToMany de dos maneras diferentes:
- Implementación de ManyToMany con List
- Implementación de ManyToMany con Set.
Relación ManyToMany en Hibernate con List
A continuación vamos a ver como implementar una relación ManyToMany con Hibernate haciendo uso de List.
@Entity @NoArgsConstructor @Getter @Setter public class Department { @Id @GeneratedValue private Long id; private String name; private String code; @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable(name = "department_employee", joinColumns = @JoinColumn(name = "department_id"), inverseJoinColumns = @JoinColumn(name = "employee_id") ) private List<Employee> employees = new ArrayList<>(); public void addEmployee(Employee employee) { employees.add(employee); employee.getDepartments().add(this); } public void removeEmployee(Employee employee) { employees.remove(employee); employee.getDepartments().remove(this); } }
@Getter @Setter @Entity @NoArgsConstructor public class Employee { @Id @GeneratedValue private Long id; private String name; @ManyToMany(mappedBy = "employees") public List<Department> departments = new ArrayList<>(); }
Las dos clases anteriores muestran la relación ManyToMany de Department con Employee, mapeada con un List.
A continuación vamos a ver los puntos más destacables:
- Al igual que en una relación @OneToMany, hemos creado dos métodos add y remove para hacer uso de una relacción bidireccional y mantener la consistencia.
- En la tabla Department únicamente hacemos uso de Merge y Persist, ya que si añadimos también remove, podríamos crear un borrado en cadena y borraría ambos lados.
- Hacemos uso de la anotación de Lombok EqualsAndHashCode.Include para hacer iguales los objetos por el Id, ya que sabemos que el Id siempre será único.
- Por último para crear la tabla intermedia hacemos uso de la anotación @JoinTable con la que le indicamos el nombre de la tabla intermedia, así como los id de las foreign key de las otras tablas. Y tenemos una relación bidireccional en la que Department es la propietaria de la relación que establecemos.
Vamos a ver con un test el resultado de ejecutar esta relación:
@SpringBootTest public class DepartmentRepositoryTest { @Autowired private DepartmentListRepository departmentListRepository; @Test public void when_save_new_department_with_multiples_employees_then_department_is_saved_correctly() { Department department = new Department(); department.setName("IT"); var employee = new Employee(); employee.setName("Noel"); department.getEmployees().add(employee); departmentListRepository.save(department); } }
Hibernate: insert into Department (code, name, id) values (?, ?, ?) Hibernate: insert into Employee (name, id) values (?, ?) Hibernate: insert into department_employee (department_id, employee_id) values (?, ?)
Como podemos ver hacemos tres insert uno por tabla.
El problema de este tipo de relación surge cuando queremos eliminar una fila en base de datos. Ya que en ese caso en lugar de eliminar una única fila, elimina todas las filas asociadas del department_employee asociado y luego introduce las restantes. Obviamente no es óptimo en temas de rendimiento y por eso no es lo mejor hacer uso de List para una relación ManyToMany.
Vamos analizar las operaciones que se hacen con un delete en un test:
@Test @Sql("classpath:createDepartment.sql") public void when_remove_employee_from_department_then_employee_is_removed_correctly() { var departmentList = departmentRepository.findAll(); var department = departmentList.stream().findFirst().orElseThrow(); var employee = department.getEmployees().stream().findFirst().orElseThrow(); department.removeEmployee(employee); departmentRepository.save(department); }
Tenemos un insert hecho previamente, mediante un fichero llamado createDepartment en nuestra carpeta resources, que cargamos con la anotación @Sql. En el test eliminamos un employee y actualizamos. Y vemos como tenemos más operaciones de las necesarias:
Hibernate: select department0_.id as id1_0_, department0_.code as code2_0_, department0_.name as name3_0_ from Department department0_ Hibernate: select employees0_.department_id as departme1_1_0_, employees0_.employee_id as employee2_1_0_, employee1_.id as id1_2_1_, employee1_.name as name2_2_1_ from department_employee employees0_ inner join Employee employee1_ on employees0_.employee_id=employee1_.id where employees0_.department_id=? Hibernate: select department0_.employee_id as employee2_1_0_, department0_.department_id as departme1_1_0_, department1_.id as id1_0_1_, department1_.code as code2_0_1_, department1_.name as name3_0_1_ from department_employee department0_ inner join Department department1_ on department0_.department_id=department1_.id where department0_.employee_id=? Hibernate: select department0_.id as id1_0_1_, department0_.code as code2_0_1_, department0_.name as name3_0_1_, employees1_.department_id as departme1_1_3_, employee2_.id as employee2_1_3_, employee2_.id as id1_2_0_, employee2_.name as name2_2_0_ from Department department0_ left outer join department_employee employees1_ on department0_.id=employees1_.department_id left outer join Employee employee2_ on employees1_.employee_id=employee2_.id where department0_.id=? Hibernate: delete from department_employee where department_id=? Hibernate: insert into department_employee (department_id, employee_id) values (?, ?)
Podemos ver en las dos últimas operaciones que hacemos un delete y luego un insert, con lo que no es muy óptimo. Esta falta de rendimiento es debido a usar list en lugar de set.
Relación ManyToMany en Hibernate con Set
A continuación vamos a solventar el problema de rendimiento que hemos tenido al crear las colecciones con List.
Para ello vamos a utilizar SET en nuestras clases de Entidad, que es la manera más recomendada de utilizar una relación ManyToMany.
@Entity @NoArgsConstructor @Getter @Setter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Department { @Id @GeneratedValue @EqualsAndHashCode.Include() private Long id; private String name; private String code; @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable(name = "department_employee", joinColumns = @JoinColumn(name = "department_id"), inverseJoinColumns = @JoinColumn(name = "employee_id") ) private Set<Employee> employees = new HashSet<>(); public void addEmployee(Employee employee) { employees.add(employee); employee.getDepartments().add(this); } public void removeEmployee(Employee employee) { employees.remove(employee); employee.getDepartments().remove(this); } }
@Getter @Setter @Entity @NoArgsConstructor @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Employee { @Id @GeneratedValue @EqualsAndHashCode.Include() private Long id; private String name; @ManyToMany(mappedBy = "employees") public Set<Department> departments = new HashSet<>(); }
Vamos a ejecutar el mismo test que hemos ejecutado anteriormente para realizar un borrado de un employee de un departamento y vamos a ver lo que ha hecho Hibernate:
Hibernate: select department0_.id as id1_2_, department0_.code as code2_2_, department0_.name as name3_2_ from DepartmentSet department0_ Hibernate: select employees0_.department_id as departme1_3_0_, employees0_.employee_id as employee2_3_0_, employeese1_.id as id1_5_1_, employeese1_.name as name2_5_1_ from departmentSet_employeeSet employees0_ inner join EmployeeSet employeese1_ on employees0_.employee_id=employeese1_.id where employees0_.department_id=? Hibernate: select department0_.employee_id as employee2_3_0_, department0_.department_id as departme1_3_0_, department1_.id as id1_2_1_, department1_.code as code2_2_1_, department1_.name as name3_2_1_ from departmentSet_employeeSet department0_ inner join DepartmentSet department1_ on department0_.department_id=department1_.id where department0_.employee_id=? Hibernate: select department0_.id as id1_2_1_, department0_.code as code2_2_1_, department0_.name as name3_2_1_, employees1_.department_id as departme1_3_3_, employeese2_.id as employee2_3_3_, employeese2_.id as id1_5_0_, employeese2_.name as name2_5_0_ from DepartmentSet department0_ left outer join departmentSet_employeeSet employees1_ on department0_.id=employees1_.department_id left outer join EmployeeSet employeese2_ on employees1_.employee_id=employeese2_.id where department0_.id=? Hibernate: delete from departmentSet_employeeSet where department_id=? and employee_id=?
Como podemos ver en las trazas anteriores únicamente hemos hecho un delete para eliminar el employee de department, por lo que el rendimiento con un Set es mucho mejor que con un List en una relación ManyToMany.
Ejemplo Spring Boot de relación ManyToMany con Set
A continuación vamos a definir un ejemplo de relación ManyToMany en Hibernate haciendo uso de Spring Boot. Este ejemplo hará uso de Set para mapear las colecciones ya que es mejor en temas de rendimiento. Para ello vamos a empezar creando el proyecto através de la página initializr.
En los siguientes apartados vamos a ir creando las capas necesarias de nuestra aplicación paso a paso.
Las dependencias que vamos a seleccionar serán Spring Web, Spring Data, Lombok y H2.
- Spring Data lleva incorporado toda la parte de la persistencia por lo que nos incopora Hibernate + JPA.
- Lombok nos ayudará a reducir el boilerplate de nuestro código.
- H2, es una base de datos en memoria perfecta para realizar pruebas, no utilizar en entornos productivos.
Para ver el ejemplo directamente lo puedes descargar de aquí.
Dependencias Maven
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
Configuración de Base de Datos H2
A continuación vamos a añadir la configuración necesaria para hacer uso de una base de nuestra Base de Datos en memoria H2. Es importante tener en cuenta que para poder acceder a la consola de H2, esta tiene que ser activada mediante la propiedad h2.console.enabled
spring: application: name: one-to-many datasource: url: jdbc:h2:mem:testdb driverClassName: org.h2.Driver username: sa password: password initialize: true initialization-mode: always hikari: connection-timeout: 6000 initialization-fail-timeout: 0 jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl properties: hibernate: show_sql: true format_sql: true enable_lazy_load_no_trans: true h2: console: enabled: true
Además vamos a hacer una importanción de datos, para ello añadimos un fichero llamado import.sql en nuestra carpeta resources.
INSERT INTO DEPARTMENT VALUES (1,'IT', 'CO'); INSERT INTO EMPLOYEE VALUES (1,'Noel'); INSERT INTO EMPLOYEE VALUES (2,'PEPE'); INSERT INTO DEPARTMENT_EMPLOYEE VALUES (1,1); INSERT INTO DEPARTMENT_EMPLOYEE VALUES (1,2);
Creación de entidades y relación ManyToMany
Para nuestro ejemplo vamos a crear dos entidades (se encuentran en dominio), estas dos entidades formarán entre si una relación ManyToMany, son Department y Employee.
La relación Department será la relación padre y hará uso de Set para establecer el mapeo y la relación bidirecciona con Employee.
@Getter @Setter @Entity @NoArgsConstructor @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Employee { @Id @GeneratedValue @EqualsAndHashCode.Include() private Long id; private String name; @ManyToMany(mappedBy = "employees") public List<Department> departments = new ArrayList<>(); }
@Entity @NoArgsConstructor @Getter @Setter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Department { @Id @GeneratedValue @EqualsAndHashCode.Include() private Long id; private String name; private String code; @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable(name = "department_employee", joinColumns = @JoinColumn(name = "department_id"), inverseJoinColumns = @JoinColumn(name = "employee_id") ) private List<Employee> employees = new ArrayList<>(); public void addEmployee(Employee employee) { employees.add(employee); employee.getDepartments().add(this); } public void removeEmployee(Employee employee) { employees.remove(employee); employee.getDepartments().remove(this); } }
Como hemos comentado anteriormente añadimos dos métodos adicionales como son add y remove para mantener la integridad entre ambas tablas. Además los métodos permitidos serán Persist y Merge, ya que si añadimos remove, podríamos causar inestabilidad en la Base de Datos eliminando objetos de ambos lados.
Creación de repositorio
Para la creación de un repositorio para bank vamos a hacer uso de JpaRepository, que es proporcionado por spring data. Esta clase nos va a permitir hacer uso de todos los métodos y acciones necesarias para hacer operaciones en nuestra base de datos.
public interface DepartmentRepository extends JpaRepository<Departmen, Long> { }
Creación del servicio
A continuación crearemos una capa service que hará de pass through, entre el controlador y el repositorio. Es decir, únicamente se encargará de pasar y propagar la información del controller al repository.
@Service @RequiredArgsConstructor public class DepartmentService { private final DepartmentRepository departmentRepository; public List<Department> findAll() { return departmentRepository.findAll(); } public Department findById(Long id) { return departmentRepository.findById(id).orElseThrow(); } public Department save (Department department) { return departmentRepository.save(department); } public void delete(Department department) { departmentRepository.delete(department); } }
Creación de un controlador para Department
A continuación vamos a crear cuatro endpoints para realizar las operaciones básicas sobre la aplicación. Por simplicidad en la aplicación no vamos a crear DTO’s con lo que haremos uso de las clases de Dominio para el controlador.
@RestController @RequiredArgsConstructor @RequestMapping("/departments") public class DepartmentController { private final DepartmentService departmentService; @GetMapping public ResponseEntity<List<Department>> getDepartments() { var departments = departmentService.findAll(); return new ResponseEntity<>(departments, HttpStatus.OK); } @GetMapping("/{id}") public ResponseEntity<Department> getDepartment(@PathVariable Long id) { var department = departmentService.findById(id); return new ResponseEntity<>(department, HttpStatus.OK); } @PostMapping() public ResponseEntity<Department> saveDepartment(@RequestBody Department department) { var departmentSaved = departmentService.save(department); return new ResponseEntity<>(departmentSaved, HttpStatus.OK); } @DeleteMapping() public ResponseEntity<Department> deleteDepartment(@RequestBody Department department) { departmentService.delete(department); return new ResponseEntity<>(department, HttpStatus.OK); } }
Conclusión
En esta entrada sobre Ejemplo de relación ManyToMany en Hibernate, hemos visto las dos aproximaciones diferentes que podemos realizar para una relación @ManyToMany en Hibernate, con Set y con List. Y como hemos podido ver la que mejor rendimiento y optimización nos va a ofrecer es aplicar a la colección un Set en lugar de un List, ya que reducimos el número de operaciones en un borrado.
Si quieres ver el ejemplo completo para una relación ManyToMany con Hibernate en Spring Boot puedes verlo en nuestro Github.
Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!
Muy útil, gracias
Hola, gracias por la explicación, pero deseo consultar en qué momento utilizas los métodos adicionales creados para mantener la sincronización pues no veo donde se utilice. Agradecería tu apoyo.
Para mantener esa «sincronización» se llamarán a los métodos add y remove cuando se añada o eliminen elementos, de esta manera podremos mantener la consistencia de los datos