Ejemplo de relación ManyToMany en Hibernate

ManyToMany con Hibernate

ManyToMany con 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:

Relación ManyToMany en Hibernate | Ejemplo de relación ManyToMany en Hibernate
Relación ManyToMany en Hibernate

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 DepartmentSet {

    @Id
    @GeneratedValue
    @EqualsAndHashCode.Include()
    private Long id;

    private String name;

    private String code;

    @ManyToMany(cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE
    })
    @JoinTable(name = "departmentSet_employeeSet",
            joinColumns = @JoinColumn(name = "department_id"),
            inverseJoinColumns = @JoinColumn(name = "employee_id")
    )
    private List<EmployeeSet> employees = new ArrayList<>();

    public void addEmployee(EmployeeSet employee) {
        employees.add(employee);
        employee.getDepartments().add(this);
    }

    public void removeEmployee(EmployeeSet employee) {
        employees.remove(employee);
        employee.getDepartments().remove(this);
    }

}
@Getter
@Setter
@Entity
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class EmployeeSet {

    @Id
    @GeneratedValue
    @EqualsAndHashCode.Include()
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "employees")
    public List<DepartmentSet> departments = new ArrayList<>();
}

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!


1 pensamiento sobre “Ejemplo de relación ManyToMany en Hibernate

Deja una respuesta

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