In this post, we are going to show an example of a Many-to-Many relationship in Hibernate. We will demonstrate how to map and use JPA through Hibernate to achieve this type of relationship.
If you need to see how to create a One-to-Many relationship, you can take a look here.
In this article, we will also explore the most efficient way to establish this type of relationship using the @ManyToMany
annotation.
How does a Many-to-Many relationship work?
A Many-to-Many relationship involves the creation of an intermediate table. For example, let’s consider two entities: “Employee” and “Department”. A department can have 1 to N employees, and an employee can belong to 1 to M departments. This results in an M to N relationship, which requires an extra table. This extra table is represented as a 1 to N relationship:
This would be the representation in the database of an N to M relationship, where we would have the two source tables and a new table formed from the other two. This new table will have the IDs referencing the other two tables.
Depending on how we implement this relationship, we can obtain different results. In this article, we will explore the implementation of the Many-to-Many relationship in two different ways:
- Implementation of Many-to-Many with List
- Implementation of Many-to-Many with Set.
Many-to-Many Relationship in Hibernate with List
Now let’s see how to implement a Many-to-Many relationship with Hibernate using 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<>(); }
The previous two classes demonstrate the ManyToMany relationship between Department and Employee, mapped with a List.
Now let’s see the most notable points:
- Similar to a @OneToMany relationship, we have created two methods, add and remove, to enable bidirectional association and maintain consistency.
- In the Department table, we only use Merge and Persist operations. If we also add remove, it could lead to cascading deletion and delete both sides.
- We use the Lombok annotation EqualsAndHashCode.Include to compare objects based on their Id, as we know the Id will always be unique.
- Finally, to create the intermediate table, we use the @JoinTable annotation, specifying the name of the intermediate table and the foreign key IDs of the other tables. We have a bidirectional relationship where Department is the owner of the established relationship.
Let’s test the result of executing this relationship:
@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 (?, ?)
As we can see, we perform three inserts, one for each table.
The problem with this type of relationship arises when we want to delete a row in the database. In that case, instead of deleting a single row, it deletes all the associated rows from the associated department_employee table and then inserts the remaining ones. Obviously, this is not optimal in terms of performance, which is why using a List for a ManyToMany relationship is not the best approach.
Let’s analyze the operations performed with a delete in a 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); }
We have a previously made insert using a file named “createDepartment” in our resources folder, which we load using the @Sql annotation. In the test, we delete an employee and update. And we can see that we have more operations than necessary:
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 (?, ?)
We can see in the last two operations that we perform a delete and then an insert, which is not very optimal. This lack of performance is due to using a List instead of a Set.
ManyToMany Relationship in Hibernate with Set
Next, we are going to solve the performance issue we encountered when creating collections with List.
To do this, we will use a Set in our Entity classes, which is the recommended way to handle a ManyToMany relationship.
@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<>(); }
We are going to execute the same test that we previously executed to delete an employee from a department, and we will see what Hibernate has done:
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=?
As we can see in the previous logs, we only performed a delete to remove the employee from the department, so the performance with a Set is much better than with a List in a ManyToMany relationship.
Example of ManyToMany Relationship with Set in Spring Boot
Next, we are going to define an example of Many-to-Many Relationship in Hibernate using Spring Boot. This example will use Set to map the collections, as it provides better performance. We will start by creating the project through the initializr page.
In the following sections, we will gradually create the necessary layers of our application.
- The dependencies we will select are Spring Web, Spring Data, Lombok, and H2.
- Spring Data includes all the persistence part, incorporating Hibernate + JPA.
- Lombok will help us reduce the boilerplate code.
H2 is an in-memory database perfect for testing purposes, not to be used in production environments.
You can directly download the example from here.
Maven Dependencies
<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>
H2 Database Configuration
Next, we are going to add the necessary configuration to make use of our in-memory H2 database. It is important to note that in order to access the H2 console, it needs to be enabled using the property 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
Additionally, we are going to perform data import. For that, we add a file named import.sql in our resources folder.
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);
Entity Creation and ManyToMany Relationship
For our example, we are going to create two entities (located in the domain package). These two entities will form a ManyToMany relationship with each other: Department and Employee.
The Department entity will be the parent relationship and will use a Set to establish the mapping and bidirectional relationship with 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); } }
As mentioned earlier, we add two additional methods, add and remove, to maintain integrity between both tables. Additionally, the allowed methods will be Persist and Merge, as adding remove could potentially cause instability in the database by deleting objects from both sides.
Repository Creation
For creating a repository for the bank, we will make use of JpaRepository, which is provided by Spring Data. This class will allow us to use all the necessary methods and actions for performing operations in our database.
public interface DepartmentRepository extends JpaRepository<Departmen, Long> { }
Service Creation
Next, we will create a service layer that will act as a pass-through between the controller and the repository. In other words, it will only be responsible for passing and propagating information from the controller to the 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); } }
Controller Creation for Department
Next, we are going to create four endpoints to perform basic operations on the application. For simplicity, we won’t create DTOs, so we will use the Domain classes for the controller.
@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); } }
Conclusion
In this post about an example of Many-to-Many Relationship in Hibernate, we have seen two different approaches we can take for a @ManyToMany relationship in Hibernate: using Set and using List. As we have seen, using Set provides better performance and optimization, as it reduces the number of operations during deletion.
If you want to see the complete example of a ManyToMany relationship with Hibernate in Spring Boot, you can find it on our GitHub page.
If you need more information, you can leave us a comment or send an email to refactorizando.web@gmail.com You can also contact us through our social media channels on Facebook or twitter and we will be happy to assist you!!