Many-to-Many mapping links records so that one record on either side can be associated with many records on the other side (one student takes many courses, one course has many students). It is used because a relational database cannot store this directly in two tables, so JPA creates a third table, called a join table, to hold the pairs of IDs. JPA manages the join table for you using @ManyToMany and @JoinTable, so you work with collections of objects instead of writing join SQL. In this experiment, a Student–Course enrollment application enrolls and un-enrolls students and reads the relationship from both directions.
Features
- The join table (
student_course) is generated automatically withstudent_idandcourse_idcolumns. - One side is the owning side, which controls the join table, and the other is the inverse side, which uses
mappedBy. Setis used for the collections, which avoids duplicate enrollments and performs better thanListin Hibernate.- Helper methods (
addCourse,removeCourse) keep both sides of the relationship in sync. - Derived queries can navigate the relationship, for example
findByCoursesId.
Many-to-Many Mapping Types
1. Unidirectional Many-to-Many. Only one entity knows about the other. For example, Student has Set<Course> courses, but Course has no student list. It is simple, but you can’t navigate from a course to its students without a query.
2. Bidirectional Many-to-Many (used in this example). Both entities hold a collection of each other. Student is the owning side, with @JoinTable. Course is the inverse side, with mappedBy = "courses". You can navigate in both directions, but you must keep both sides in sync using helper methods.
3. Many-to-Many with extra columns (join entity). When the relationship itself needs data, such as enrollment date or grade, the plain @ManyToMany is replaced by a third entity, Enrollment. It has one @ManyToOne to Student and one to Course, plus the extra fields. Use it when the join table needs more than two ID columns.
Hands-on Experiment: Creating a spring boot application with Many-to-Many Mapping
The following steps are involved in developing this hands-on experiment.
Step 1: Generate the Project
Step 2: Project Structure
Step 3: pom.xml
Step 4: application.properties
Step 5: Create Entity Class
Step 6: Create Repository Interface
Step 7: Create Service Class
Step 8: Create Controller Class
Step 9: Main Application Class
Step 10: Run the Main Application Class
Step 1: Configure the Project on Spring Initializr
Go to start.spring.io and set:
- Project: Maven
- Language: Java
- Spring Boot version: 3.2.x
- Group: com.example
- Artifact: enrollment
- Name: enrollment
- Package name: com.example.enrollment
- Packaging: Jar
- Java: 17
- Dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver
Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.
Step 2: Generated Project Structure

Step 3: Explore pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>com.javaplanet</groupId>
<artifactId>student-course-manytomany</artifactId>
<version>1.0.0</version>
<name>student-course-manytomany</name>
<description>Many-to-Many Mapping - Student Course Enrollment</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 4: application.properties
spring.application.name=student-course-manytomany
server.port=8082
spring.datasource.url=jdbc:mysql://localhost:3306/enrollment_db?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialectCode language: JavaScript (javascript)
Step 5: Create Entity Class
package com.javaplanet.enrollment.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private int credits;
// INVERSE side: mappedBy points to the field on the owning side
@ManyToMany(mappedBy = "courses")
@JsonIgnoreProperties("courses")
private Set<Student> students = new HashSet<>();
public Course() {
}
public Course(String title, int credits) {
this.title = title;
this.credits = credits;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public int getCredits() { return credits; }
public void setCredits(int credits) { this.credits = credits; }
public Set<Student> getStudents() { return students; }
public void setStudents(Set<Student> students) { this.students = students; }
}package com.javaplanet.enrollment.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
// OWNING side: this side defines the join table
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
@JsonIgnoreProperties("students")
private Set<Course> courses = new HashSet<>();
public Student() {
}
public Student(String name, String email) {
this.name = name;
this.email = email;
}
// helper methods keep both sides of the relationship in sync
public void addCourse(Course course) {
this.courses.add(course);
course.getStudents().add(this);
}
public void removeCourse(Course course) {
this.courses.remove(course);
course.getStudents().remove(this);
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Set<Course> getCourses() { return courses; }
public void setCourses(Set<Course> courses) { this.courses = courses; }
}
Step 6: Create below Repository Interface
package com.javaplanet.enrollment.repository;
import com.javaplanet.enrollment.entity.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
// all courses taken by a given student (navigates Course.students.id)
List<Course> findByStudentsId(Long studentId);
}
package com.javaplanet.enrollment.repository;
import com.javaplanet.enrollment.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// all students enrolled in a given course (navigates Student.courses.id)
List<Student> findByCoursesId(Long courseId);
}
Step 7: Create below Service class
package com.javaplanet.enrollment.service;
import com.javaplanet.enrollment.entity.Course;
import com.javaplanet.enrollment.entity.Student;
import com.javaplanet.enrollment.repository.CourseRepository;
import com.javaplanet.enrollment.repository.StudentRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Service
public class CourseService {
private final CourseRepository courseRepository;
private final StudentRepository studentRepository;
public CourseService(CourseRepository courseRepository, StudentRepository studentRepository) {
this.courseRepository = courseRepository;
this.studentRepository = studentRepository;
}
public Course createCourse(Course course) {
return courseRepository.save(course);
}
@Transactional(readOnly = true)
public List<Course> getAllCourses() {
return courseRepository.findAll();
}
@Transactional(readOnly = true)
public Course getCourse(Long id) {
return courseRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Course not found: " + id));
}
@Transactional(readOnly = true)
public List<Student> getStudentsOfCourse(Long courseId) {
getCourse(courseId);
return studentRepository.findByCoursesId(courseId);
}
}
package com.javaplanet.enrollment.service;
import com.javaplanet.enrollment.entity.Course;
import com.javaplanet.enrollment.entity.Student;
import com.javaplanet.enrollment.repository.CourseRepository;
import com.javaplanet.enrollment.repository.StudentRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Service
public class StudentService {
private final StudentRepository studentRepository;
private final CourseRepository courseRepository;
public StudentService(StudentRepository studentRepository, CourseRepository courseRepository) {
this.studentRepository = studentRepository;
this.courseRepository = courseRepository;
}
public Student createStudent(Student student) {
return studentRepository.save(student);
}
@Transactional(readOnly = true)
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
@Transactional(readOnly = true)
public Student getStudent(Long id) {
return studentRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Student not found: " + id));
}
@Transactional
public Student enroll(Long studentId, Long courseId) {
Student student = getStudent(studentId);
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Course not found: " + courseId));
student.addCourse(course);
return studentRepository.save(student);
}
@Transactional
public Student unenroll(Long studentId, Long courseId) {
Student student = getStudent(studentId);
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Course not found: " + courseId));
student.removeCourse(course);
return studentRepository.save(student);
}
@Transactional(readOnly = true)
public List<Course> getCoursesOfStudent(Long studentId) {
getStudent(studentId);
return courseRepository.findByStudentsId(studentId);
}
}
Step 8: Create Controller class
package com.javaplanet.enrollment.controller;
import com.javaplanet.enrollment.entity.Course;
import com.javaplanet.enrollment.entity.Student;
import com.javaplanet.enrollment.service.CourseService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/courses")
public class CourseController {
private final CourseService courseService;
public CourseController(CourseService courseService) {
this.courseService = courseService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Course create(@RequestBody Course course) {
return courseService.createCourse(course);
}
@GetMapping
public List<Course> getAll() {
return courseService.getAllCourses();
}
@GetMapping("/{id}")
public Course getOne(@PathVariable Long id) {
return courseService.getCourse(id);
}
@GetMapping("/{courseId}/students")
public List<Student> getStudents(@PathVariable Long courseId) {
return courseService.getStudentsOfCourse(courseId);
}
}
package com.javaplanet.enrollment.controller;
import com.javaplanet.enrollment.entity.Course;
import com.javaplanet.enrollment.entity.Student;
import com.javaplanet.enrollment.service.StudentService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Student create(@RequestBody Student student) {
return studentService.createStudent(student);
}
@GetMapping
public List<Student> getAll() {
return studentService.getAllStudents();
}
@GetMapping("/{id}")
public Student getOne(@PathVariable Long id) {
return studentService.getStudent(id);
}
@PostMapping("/{studentId}/courses/{courseId}")
public Student enroll(@PathVariable Long studentId, @PathVariable Long courseId) {
return studentService.enroll(studentId, courseId);
}
@DeleteMapping("/{studentId}/courses/{courseId}")
public Student unenroll(@PathVariable Long studentId, @PathVariable Long courseId) {
return studentService.unenroll(studentId, courseId);
}
@GetMapping("/{studentId}/courses")
public List<Course> getCourses(@PathVariable Long studentId) {
return studentService.getCoursesOfStudent(studentId);
}
}
Step 9: Main Application
package com.javaplanet.enrollment;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EnrollmentApplication {
public static void main(String[] args) {
SpringApplication.run(EnrollmentApplication.class, args);
}
}
Step 10: Run the EnrollmentApplication


