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

In this article, we will learn how to use many to many mapping in JPA. We will create RESTful endpoints to perform CRUD operation 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. 

OK!, It’s time for action 🙂 Let’s begin 🙂

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 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 following 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

Next step involves creating the database tables and setting up the database configuration.

Create Student, Course and student_Course join table on database.

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

Below is 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 to application.properties

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 Student.java class. This class will contain id, student’s name and a set of courses student has opted.

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 JPA @ManyToMany annotation.
  • Here, we have 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 map the join column of the join table using joinColumns property. In our case, the value of 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 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 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 Course.java entity class. This class will 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 @ManyToMany annotation on the Course.java 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 access all the associated students from the course side as well.

Create Repository, Service and Controller layers.

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

Create JPA Repository

Let us create a StudentRepository.java interface. We are 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 CourseRepository.java interface and add the below content.

We are 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 StudentDto.java class. This class will be used 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 created, we can use it inside the service layer by auto wiring it.

Create a StudentService.java interface.

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 class called StudentServiceImpl,java

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) {
		
		Student student = studentRepository.getOne(studentId);
		//Remove the related courses from student entity.
		student.removeCourses();
		studentRepository.deleteById(studentId);
		return "Student with id: " + studentId + " deleted successfully!";
	}

	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. 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

Final step is to create RESTful endpoints for CRUD operation.

Below is the StudentController.java class 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> updateEmployee(@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. Now we can test the RESTful endpoints with the help of the postman tool.

Testing 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 employee entity.

jpa many to many example

Conclusion

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

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

Sample code is available on github. Happy coding!! 🙂

You may also interested in