Mastering Bidirectional Many-to-Many Relation and Hidden Secret in Spring Boot

Building a Spring Boot application often involves managing complex relationships between entities, and one common scenario is the many-to-many relationship. In this comprehensive guide, we’ll explore the intricacies of bidirectional many-to-many relationships using Spring Boot and JPA. By the end of this blog, you’ll have a deep understanding of the concepts and a practical, code-centric approach to implement bidirectional relationships in your Spring Boot projects.

Understanding Bidirectional Many-to-Many Relationships

Before we dive into the code, let’s briefly understand the concept of bidirectional many-to-many relationships. In a bidirectional relationship, entities on both sides can navigate to each other. If Entity A is related to Entity B, then Entity B is also related to Entity A. This is achieved by maintaining two sets of references, one on each side of the relationship.

Why Bidirectional?

Bidirectional relationships provide flexibility and ease of navigation. When retrieving data, you can traverse the relationship from either side, allowing for more natural and intuitive code. Additionally, bidirectional relationships often lead to more efficient database queries.

Setting Up the Project

Let’s start by creating a new Spring Boot project. You can use Spring Initializr (https://start.spring.io/) or your preferred IDE. Include the following dependencies:

  • Spring Web
  • Spring Data JPA
  • H2 Database (for simplicity, but you can choose any other database)

Once the project is set up, let’s create two entities: Student and Course. A student can enroll in multiple courses, and a course can have multiple students.

// Student.java
@Entity
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(cascade = { CascadeType.ALL })
    @JoinTable(
        name = "student_course",
        joinColumns = { @JoinColumn(name = "student_id") },
        inverseJoinColumns = { @JoinColumn(name = "course_id") }
    )
    private Set<Course> courses = new HashSet<>();

    // Constructors, getters, and setters
}
// Course.java
@Entity
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();

    // Constructors, getters, and setters
}

In the Student class, the @ManyToMany annotation is used to define the relationship with the Course entity. The cascade attribute ensures that changes to one side of the relationship are cascaded to the other side. In the Course class, the mappedBy attribute indicates that the relationship is bidirectional and is managed by the courses property in the Student class.

Repository and Service Layers

Now, let’s create repositories and service classes for both entities to handle database operations.

// StudentRepository.java
public interface StudentRepository extends JpaRepository<Student, Long> {
}
// CourseRepository.java
public interface CourseRepository extends JpaRepository<Course, Long> {
}
// StudentService.java
@Service
public class StudentService {

    @Autowired
    private StudentRepository studentRepository;

    public List<Student> getAllStudents() {
        return studentRepository.findAll();
    }

    // Other business logic as needed
}
// CourseService.java
@Service
public class CourseService {

    @Autowired
    private CourseRepository courseRepository;

    public List<Course> getAllCourses() {
        return courseRepository.findAll();
    }

    // Other business logic as needed
}

The repository interfaces extend JpaRepository to inherit common CRUD operations. Service classes are responsible for encapsulating business logic and managing transactions.

Controllers and Views

Create controllers to handle HTTP requests and Thymeleaf templates for data presentation.

// StudentController.java
@Controller
@RequestMapping("/students")
public class StudentController {

    @Autowired
    private StudentService studentService;

    @GetMapping
    public String getAllStudents(Model model) {
        List<Student> students = studentService.getAllStudents();
        model.addAttribute("students", students);
        return "students";
    }

    // Other controller methods as needed
}
// CourseController.java
@Controller
@RequestMapping("/courses")
public class CourseController {

    @Autowired
    private CourseService courseService;

    @GetMapping
    public String getAllCourses(Model model) {
        List<Course> courses = courseService.getAllCourses();
        model.addAttribute("courses", courses);
        return "courses";
    }

    // Other controller methods as needed
}

The controllers use Thymeleaf templates to render data. For simplicity, let’s create basic templates.

<!-- students.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Students</title>
</head>
<body>
    <h2>Students</h2>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Courses</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="student : ${students}">
                <td th:text="${student.id}"></td>
                <td th:text="${student.name}"></td>
                <td>
                    <ul>
                        <li th:each="course : ${student.courses}" th:text="${course.name}"></li>
                    </ul>
                </td>
            </tr>
        </tbody>
    </table>
</body>
</html>
<!-- courses.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Courses</title>
</head>
<body>
    <h2>Courses</h2>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Students</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="course : ${courses}">
                <td th:text="${course.id}"></td>
                <td th:text="${course.name}"></td>
                <td>
                    <ul>
                        <li th:each="student : ${course.students}" th:text="${student.name}"></li>
                    </ul>
                </td>
            </tr>
        </tbody>
    </table>
</body>
</html>

Testing and Running the Application

With the project structure in place, run your Spring Boot application and navigate to http://localhost:8080/students and http://localhost:8080/courses in your web browser.

Conclusion

Congratulations! You’ve successfully implemented a bidirectional many-to-many relationship in a Spring Boot application. This approach provides flexibility and ease of navigation, allowing you to design more intuitive and efficient code.

Understanding how to model and manage complex relationships is crucial in building robust and scalable applications. The code-centric approach provided in this guide serves as a practical foundation for incorporating bidirectional relationships into your Spring Boot projects