JPA Many to Many Example – Spring Boot

A many to many JPA entity relationship is where both related entities can have multiple related entities. We also use join tables to associate these entities.

In this article, we will learn how to use many to many mapping in JPA. We will also create RESTful endpoints to perform CRUD operations on these related entities.

We are going to create Student and Course entity classes with a join table to map the “many to many” JPA relationship. This is one good example as a student can opt for many courses. Also, a single course can have multiple students opted for it. 

Create a Spring boot application

Since we are going to use spring boot to demonstrate the JPA many to many entity relationships, the first step is to create a spring boot application with required dependencies.

Create a spring boot application with the name jpa-many-to-many-example with required dependencies.

We will need spring-boot-starter-web, spring-boot-starter-data-jpa dependencies, and also the Lombok dependency to reduce the boilerplate code.

We will be going to use the PostgreSQL database, so we need to add that dependency too to our spring boot application.

The below pom xml file contains all the required dependencies.

<?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>2.1.8.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.asb.example</groupId>
	<artifactId>jpa-many-to-many-example</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>jpa-many-to-many-example</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
		<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
	</properties>
	<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>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

Create database tables and setup database configuration

The next step involves creating the database tables and setting up the database configuration.

Create Student, Course, and student_Course join table on the database.

A join table is also a table that joins two tables by referring to their id columns.

Finally, below are the SQL script to create the tables and required table sequences.

CREATE TABLE STUDENT(
ID INT PRIMARY KEY NOT NULL,
NAME VARCHAR
);
CREATE TABLE COURSE(
ID INT PRIMARY KEY NOT NULL,
NAME VARCHAR
);
CREATE SEQUENCE course_sequence;
CREATE SEQUENCE student_sequence;
CREATE TABLE STUDENT_COURSE(
STUDENT_ID INT NOT NULL,
COURSE_ID INT NOT NULL,
PRIMARY KEY(STUDENT_ID, COURSE_ID)
);

Add required datasource configuration

spring.datasource.url=jdbc:postgresql://localhost/postgres
spring.datasource.username=postgres
spring.datasource.password=asbnotebook
#Enable physical naming strategy.
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

We are setting Datasource URL, username, and password to connect to the PostgreSQL database. We are also using hibernate physical naming strategy implementation.

Create Entity classes and add JPA entity mapping

The next step is to create the entity classes and define the JPA relationship between them.

Let’s create a java class with the name Student. This class will contain the id, student’s name, and also a set of courses the student has opted for.

package com.asb.example.model;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "STUDENT")
public class Student {
	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "student_sequence")
	@SequenceGenerator(name = "student_sequence", sequenceName = "student_sequence")
	private Integer id;
	@Column(name = "name")
	private String name;
	@ManyToMany(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
	@JoinTable(name = "STUDENT_COURSE", joinColumns = { @JoinColumn(name = "STUDENT_ID") }, inverseJoinColumns = {
			@JoinColumn(name = "COURSE_ID") })
	private Set<Course> courses;
	public void addCourse(Course course) {
		this.courses.add(course);
		course.getStudents().add(this);
	}
	public void removeCourse(Course course) {
		this.getCourses().remove(course);
		course.getStudents().remove(this);
	}
	public void removeCourses() {
		for (Course course : new HashSet<>(courses)) {
			removeCourse(course);
		}
	}
}
  • A student can have a set of courses. Also, multiple students can attend one particular course. This relationship is represented using the @ManyToMany JPA annotation.
  • Here, we have also used the @JoinTable annotation to map the join table, which relates the Student and Course entities using the foreign keys, pointing to the primary key of each entity class.
  • We also map the join column of the join table using the joinColumns property. In our case, the value of the joinColumn property is STUDENT_ID
  • The inverse column refers to the inverse side of the JPA entity relationship. We use the inverseJoinColumns property to set the inverse column details.
  • The join table STUDENT_COURSE also contains the STUDENT_ID column, that refers to the primary key of the STUDENT table.
  • We are not using cascade type ALL, as this may propagate the delete operation to the courses as well and deletes all the associated courses if a student object is deleted.
  • The addCourse() and the removeCourse() methods are used to create/remove association with the course entity for a particular student entity.
  • The removeCourses() method is used to remove the mapping of the existing courses with the student entity before deleting it.
  • Note that deleting the student entity only removes student entity and the mapped courses to that entity.

Create a java entity class with the name Course.

The class will also contain id, course name, and a set of students.

package com.asb.example.model;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "COURSE")
public class Course {
	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "course_sequence")
	@SequenceGenerator(name = "course_sequence", sequenceName = "course_sequence")
	private Integer id;
	@Column(name = "name")
	private String name;
	@ManyToMany(mappedBy = "courses")
	@JsonIgnore
	private Set<Student> students;
}
  • We will use the @ManyToMany annotation on the Course entity class with the mappedBy attribute.
  • The mappedBy attribute indicates that this class is the inverse-side of Many to many relationships.
  • With this, we can also access all the associated students from the course side as well.

Create Repository, Service and Controller layers.

Also, we are exposing RESTful endpoints to create, retrieve, update and delete student and course records. We will create the required JPA repository, service, and Rest controller layers.

Create JPA Repository

Let us create an interface with the name StudentRepository.

We are also extending the JpaRepository interface, which gives the required basic support for CRUD operations.

package com.asb.example.repo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.asb.example.model.Student;
@Repository
public interface StudentRepository extends JpaRepository<Student, Integer> {
}

Create a java interface with the name CourseRepository, and add the below content.

We are also using the findByName() method to retrieve a course by passing the course name.

package com.asb.example.repo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.asb.example.model.Course;
@Repository
public interface CourseRepository extends JpaRepository<Course, Integer> {
	public Course findByName(String courseName);
}

Create a DTO class

Create a java class with the name StudentDto. We use this class for exposing the student and course details to the REST endpoint.

The DTO class contains id, name, and a set of course names.

package com.asb.example.dto;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class StudentDto {
	private Integer id;
	private String name;
	private Set<String> courses = new HashSet<>();
}

Create service layer

Once the required repository layer is ready, we can use it inside the service layer by auto-wiring it.

Create a StudentService interface as shown below.

package com.asb.example.service;
import java.util.List;
import com.asb.example.dto.StudentDto;
public interface StudentService {
	public StudentDto addStudent(StudentDto studentDto);
	public List<StudentDto> getAllStudents();
	public StudentDto updateStudent(Integer studentId, StudentDto student);
	public String deleteStudent(Integer studentId);
}

Create an implementation java class with the name StudentServiceImpl.

package com.asb.example.service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.asb.example.dto.StudentDto;
import com.asb.example.model.Course;
import com.asb.example.model.Student;
import com.asb.example.repo.CourseRepository;
import com.asb.example.repo.StudentRepository;
@Service
public class StudentServiceImpl implements StudentService {
	@Resource
	private StudentRepository studentRepository;
	@Resource
	private CourseRepository courseRepository;
	@Transactional
	@Override
	public StudentDto addStudent(StudentDto studentDto) {
		Student student = new Student();
		mapDtoToEntity(studentDto, student);
		Student savedStudent = studentRepository.save(student);
		return mapEntityToDto(savedStudent);
	}
	@Override
	public List<StudentDto> getAllStudents() {
		List<StudentDto> studentDtos = new ArrayList<>();
		List<Student> students = studentRepository.findAll();
		students.stream().forEach(student -> {
			StudentDto studentDto = mapEntityToDto(student);
			studentDtos.add(studentDto);
		});
		return studentDtos;
	}
	@Transactional
	@Override
	public StudentDto updateStudent(Integer id, StudentDto studentDto) {
		Student std = studentRepository.getOne(id);
		std.getCourses().clear();
		mapDtoToEntity(studentDto, std);
		Student student = studentRepository.save(std);
		return mapEntityToDto(student);
	}
	@Override
	public String deleteStudent(Integer studentId) {
		Optional<Student> student = studentRepository.findById(studentId);
		//Remove the related courses from student entity.
		if(student.isPresent()) {
			student.get().removeCourses();
			studentRepository.deleteById(student.get().getId());
			return "Student with id: " + studentId + " deleted successfully!";
		}
		return null;
	}
	private void mapDtoToEntity(StudentDto studentDto, Student student) {
		student.setName(studentDto.getName());
		if (null == student.getCourses()) {
			student.setCourses(new HashSet<>());
		}
		studentDto.getCourses().stream().forEach(courseName -> {
			Course course = courseRepository.findByName(courseName);
			if (null == course) {
				course = new Course();
				course.setStudents(new HashSet<>());
			}
			course.setName(courseName);
			student.addCourse(course);
		});
	}
	private StudentDto mapEntityToDto(Student student) {
		StudentDto responseDto = new StudentDto();
		responseDto.setName(student.getName());
		responseDto.setId(student.getId());
		responseDto.setCourses(student.getCourses().stream().map(Course::getName).collect(Collectors.toSet()));
		return responseDto;
	}
}
  • The service layer is used for CRUD operations on the student entity.
  • The addStudent() method is used to persist the Student and related course entities into the database.
  • The addStudent() method converts the StudentDto object into entity by calling mapDtoToEntity() method.
  • The mapDtoToEntity() method adds courses to the newly created student entity by fetching the course from the database if it exists. Also, a new course entity is created and associated with the student entity if the course entity is not available.
  • The mapEntityToDto() method is used to convert the entity object back to the DTO object.
  • The getAllStudents() method returns all the available students in the database along with the courses.
  • The updateStudent() method fetches the existing student entity from the database and updates it.
  • The deleteStudent() method fetches the existing student entity by passing the id.
  • We are calling the removeCourses() method of student entity to remove the existing courses before deleting the entity. This will delete the mapping record from the join table without deleting the course entity.

Create controller layer

The final step is to create RESTful endpoints for the CRUD operation.

Below is the java controller class StudentController which supports CRUD operation on the Student entity.

package com.asb.example.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.asb.example.dto.StudentDto;
import com.asb.example.service.StudentService;
@RestController
public class StudentController {
	@Autowired
	private StudentService studentService;
	@GetMapping("/students")
	public ResponseEntity<List<StudentDto>> getAllStudents() {
		List<StudentDto> students = studentService.getAllStudents();
		return new ResponseEntity<>(students, HttpStatus.OK);
	}
	@PostMapping("/student")
	public ResponseEntity<StudentDto> getAllStudents(@RequestBody StudentDto studentDto) {
		StudentDto std = studentService.addStudent(studentDto);
		return new ResponseEntity<>(std, HttpStatus.CREATED);
	}
	@PutMapping("/student/{id}")
	public ResponseEntity<StudentDto> updateStudent(@PathVariable(name = "id") Integer id,
			@RequestBody StudentDto student) {
		StudentDto std = studentService.updateStudent(id, student);
		return new ResponseEntity<>(std, HttpStatus.CREATED);
	}
	@DeleteMapping("/student/{id}")
	public ResponseEntity<String> deleteStudent(@PathVariable(name = "id") Integer studentId) {
		String message = studentService.deleteStudent(studentId);
		return new ResponseEntity<>(message, HttpStatus.OK);
	}
}

It’s time to run the application! 🙂

Start the spring boot application. Also, we can test the RESTful endpoints with the help of the postman tool.

Test the CRUD operation

It’s time to test the CRUD RESTful endpoints. Let’s test it with the help of the postman tool.

CREATE

Below is the create operation of student entity, with a course. We are saving a student entity with a course in this example.

jpa many to many spring boot example

Below is the screenshot of SQL queries generated by the JPA.

many to many jpa insert sql query

READ

The below image shows the read operation of a created employee record, along with the course details.

many to many jpa example

UPDATE

The below image shows the update operation of the employee entity.

update jpa many to many

DELETE

The below image shows the delete operation of the employee entity.

jpa many to many example

Conclusion

In conclusion, In this article, we learned how to use the @ManyToMany annotation to represent the many to many JPA relationship.

We also performed the CRUD operation on the entity which has many to many relationships with another entity.

Finally, the sample code is available on github.

5 comments

  1. Great tutorial! Are you able to show an example of how to get all students by course, or get all courses by student_id?

  2. I have done the same controller and CourseService with Course but cannot delete a Course as it concerns Students. The code is still running fine (no exceptions are given)

Comments are closed.