In this article, we will learn how to implement Many-To-Many Relationship mapping with Spring Boot JPA. Let’s dive into it.
Spring JPA Many to Many Relationship
Mant-To-Many Relationship exist when multiple records in one table have an association with other tables. Let’s take an example:
As you can see in the picture, i have taken a general relationship representation for post and category. A post can be associated with multiple category and vice-versa. We are going to use the same two Entities Post and Category for our Spring JPA Many to Many relationship illustration.
@ManyToMany annotation is used to link two entities which we will be discussing more about later in the course.
Database Relationship
In our example, we will be using H2 database, you can use MySQL or PostgreSQL as you like. I will walk you through stepwise for each database.There are three tables we will be dealing with post, category and post_category.
Post is the tables that persists post metadata, category persist different category data and post_category is the table which links post and category. post_id is primary key of Post entity, category_id is primary key of Category and this two primary key joins to form post_category table which is the actual representation of two tables connection.
Setup Spring Boot JPA Project
Let’s use Spring Starter tool to generate a project with one click. You can add the three dependency we need while generating the project: Spring Web, Spring Data JPA. I will be using Intellij. You can use any development tools you like for example Spring Tool Suite or Eclipse.
Once you download the project, your pom should look like this:
<?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.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.rtech</groupId>
<artifactId>ManyToManyRelationShip.SpringBoot.JPA</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ManyToManyRelationShip.SpringBoot.JPA</name>
<description>Demo project for Spring Boot JPA Many To Many Relationship</description>
<properties>
<java.version>11</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>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>
If you are planning to build project from scratch then, you can add the libraries manually as mentioned above.
Configure Spring Boot JPA with H2, MySQL or PostgreSQL
We need one more dependency for datasource. As i have mentioned above, you can use any datasource you like and for this part we need to add one more dependency depending on the datasource.
For MySQL
Let’s add MySQL dependency in pom.xml.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
Along with it, we also need to update application.properties file.
spring.datasource.url= jdbc:mysql://localhost:3306/your_database_name?useSSL=false
spring.datasource.username= your_username
spring.datasource.password= your_password
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto= update
For PostgreSQL
To add PostgreSQL dependency in pom.xml.
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Along with it, we also need to update application.properties file.
spring.datasource.url=jdbc:postgresql://localhost:5432/your_database_name
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation= true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto = update
spring.jpa.show-sql= true
For H2
To add H2 dependency in pom.xml.
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Along with it, we also need to update application.properties file.
spring.datasource.url=jdbc:h2:mem:your_database_name //i will be using jpadb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto= update
spring.h2.console.enabled=true
# h2-console is default path for H2
- spring.datasource.url is used to define database url. “jdbc:h2:mem:[your_database_name]” is how you define the url for H2.
- spring.datasource.username and spring.datasource.password are the credentials to connect to database.
- spring.jpa.hibernate.ddl-auto is used to initialize database. It will create database tables automatically based on our defined model. You can also set this property to following values: none, validate, update, create-drop. For production, it is better to use validate as property.
- spring.h2.console.enabled=true will tell Spring to start H2 Database administration tool. We can access this on http://localhost:8080/h2-console
- spring.jpa.show-sql=true is to log database transaction on the console
- spring.jpa.properties.hibernate.dialect is to configure Hibernate for JPA implementation. It’s different for MySQL and PostgreSQL.
Define Data Model class for Spring JPA Many to Many relationship
In model package, we will define two classes named Post and Category. You might be thinking why don’t we create the class for association between both of the table/classes, spring jpa will take care of that. I will be defining these inside package named model.
Post
package com.rtech.ManyToManyRelationShip.SpringBoot.JPA.model;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "post")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "description")
private String description;
@Column(name = "author")
private String author;
@ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name = "post_category",
joinColumns = {@JoinColumn(name = "post_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "category_id", referencedColumnName = "id")})
private Set<Category> categories = new HashSet<>();
public Post() {
}
public Post(String name, String description, String author) {
this.name = name;
this.description = description;
this.author = author;
}
public Post(String name, String description, String author, Set<Category> categories) {
this.name = name;
this.description = description;
this.author = author;
this.categories = categories;
}
//removed getter and setter for brevity
//Need to add getter and setter if are using the same code.
public void setCategories(Set<Category> categories) {
this.categories = categories;
}
public void addCategory(Category category) {
this.categories.add(category);
category.getPosts().add(this);
}
}
Category
I am defining the class inside model package again.
package com.rtech.ManyToManyRelationShip.SpringBoot.JPA.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "category")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String name;
@ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
}, mappedBy = "categories")
@JsonIgnore
private Set<Post> posts = new HashSet<>();
public Category(){}
public Category(String name) {
this.name = name;
}
//removed getter and setter for brevity
//Need to add getter and setter if are using the same code.
}
@Entity
This annotation tells Spring JPA that the class is a persistent Java class and the table has to be mapped on to the database.
@Table
The annotation maps the class name to a table name in datasource. “name” property defines table name.
@Id
A table needs to have a unique key as an attribute based on which you can perform query for uniqueness. @Id annotation tells that the column is a primary key. The primary key can be generated by different ways. @GeneratedValue defines how the primary key will be generated.
@Column
Table is supposed to have different columns and this column is mapped to attributes name in Java persistent class. @Column annotation tells Spring JPA that the attribute is a column of the associated table. With name property column name can be given too.
@ManyToMany
As discussed above to show the relationship between two tables as ManyToMany, @ManyToMany annotation is applied to the attribute that associates the relationship between two entities. This annotation has to be applied in both class.
@JoinTable
In ManyToMany relationship, one need to define only two class and the third class/table that holds the relationship of two class is created by Spring JPA automatically. And to tell Spring JPA about the name of the table, we use @JoinTable annotation. @JoinTable annotation creates a post_category table that house two attributes defined above: post_id, category_id. This will generate a separate table based on given name in “name” property which stores primary keys of Post and Category class.
@joinColumns
@joinColumns annotation holds primary key of owning table and inverseJoinColumns holds primary key of the next entity. These two property tells Spring JPA which column to associates as primary key in ManyToMany relationship.
For every relationship of many to many, there is one entity which is the owner and the next is non-owner side. This distinction is defined by @joinColumns annotation as mentioned above within @JoinTable annotation.You can also define mappedBy attribute on the inverse side of the two entity which tells that I am not the owner.
Cascade
Cascading is the relationship between two entity. Let’s say we have Person and Address as two entity. Address does not exist without Person and hence if person is deleted then Address has to be deleted.
There are different types of JPA cascade types: for example: all, persist, merge, remove, refresh etc. CascadeType.ALL propagates all operations from parent to child entity. CascadeType.PERSIST propagates save operation from a parent to a child entity. CascadeType.REMOVE removes child entity row if parent is removed.
FetchType
In eager loading if we load the class or parent entity then all the attributes or child entity is also loaded and stored in memory but with lazy loading child entity is loaded only with an explicit call. Hibernate provides lazy loading approach by proxy implementation on the classes.
Eager loading is a design pattern which initializes data at the very first. Lazy loading is a design pattern which defers data initialization until it is required.
Hibernate will do all the magic of creating of record once the owner table decide to create a record. The record in association table is automatically generated once changes is made on the owner side.
Define Repository Interface Spring JPA
Inside the repository package, let’s create two interface that extends JpaRepository; CategoryRepository and PostRepository. These repository is defined under repository package.
It provides some Generic Crud operations like save(), findById(), deleteById(), findAll() and so on. There are many such method which comes out of the box and can use without even implementing.
PostRepository
package com.rtech.ManyToManyRelationShip.SpringBoot.JPA.repository;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Integer> {
//can add any custom interface as required
}
CategoryRepository
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.model.Category;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CategoryRepository extends JpaRepository<Category, Integer> {
//can add any custom interface as required
}
Define Spring Rest Controller for Spring JPA
In this section, we will be creating a simple two controller for Category and Post. These controllers is defined under controller package.
PostController
package com.rtech.ManyToManyRelationShip.SpringBoot.JPA.controller;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.model.Category;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.model.Post;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.repository.CategoryRepository;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@RestController
public class PostController {
@Autowired
PostRepository postRepository;
@Autowired
CategoryRepository categoryRepository;
@GetMapping("/post/{id}")
public ResponseEntity<Post> getPostById(@PathVariable("id") int id) {
Optional<Post> post = postRepository.findById(id);
if(post.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(post.get(), HttpStatus.OK);
}
@PostMapping("/post")
public ResponseEntity<Post> createPost(@RequestBody Post post) {
List<Category> categories;
if(post.getCategories().size() != 0) {
categories = post.getCategories().stream().map((category -> {
if(category.getId() == null) {
return categoryRepository.save(category);
}
return category;
})).collect(Collectors.toList());
}
Post resPost = postRepository.save(new Post(post.getName(), post.getDescription(), post.getAuthor(), post.getCategories()));
return new ResponseEntity<>(resPost, HttpStatus.CREATED);
}
@DeleteMapping("/post/{id}")
public ResponseEntity<HttpStatus> deletePostById(@PathVariable("id") int id) {
postRepository.deleteById(id);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
CategoryController
package com.rtech.ManyToManyRelationShip.SpringBoot.JPA.controller;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.model.Category;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.model.Post;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.repository.CategoryRepository;
import com.rtech.ManyToManyRelationShip.SpringBoot.JPA.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@RestController
public class CategoryController {
@Autowired
CategoryRepository categoryRepository;
@Autowired
PostRepository postRepository;
@GetMapping("/category/{id}")
public ResponseEntity<Category> getCategoryById(@PathVariable("id") int id) {
Optional<Category> category = categoryRepository.findById(id);
if(category.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(category.get(), HttpStatus.OK);
}
@PostMapping("/post/{postId}/category")
public ResponseEntity<Category> createCategory(@PathVariable("postId") int id,
@RequestBody Category reqCategory) {
Optional<Post> optionalPost = postRepository.findById(id);
if(optionalPost.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
Post post = optionalPost.get();
Optional<Category> resCategory = categoryRepository.findById(id);
Category category;
if(resCategory.isEmpty()) {
category = categoryRepository.save(new Category(reqCategory.getName()));
post.addCategory(category);
postRepository.save(post);
return new ResponseEntity<>(category, HttpStatus.CREATED);
} else {
post.addCategory(resCategory.get());
postRepository.save(post);
return new ResponseEntity<>(resCategory.get(), HttpStatus.OK);
}
}
@DeleteMapping("/category/{id}")
public ResponseEntity<HttpStatus> deleteCategoryById(@PathVariable("id") int id) {
categoryRepository.deleteById(id);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
Testing of Many To Many Relationship
Project structure
This is how your project will look like once you build the project.
Postman testing
Once you run the application, it will be exposed on 8080 port and will be using postman to test the functionalities.
Postman: Create request for Post
Postman: Get request for Post
Postman: Get request for Category
Conclusion
In this article we built a Spring JPA project implementing many-to-many relationship with different database. Let me know if you found this article helpful through comments.