In this tutorial, I will show you how to implement Spring JPA Many-To-Many mapping with Hibernate in a Spring Boot CRUD example using @ManyToMany
annotation. You’ll know:
- How to configure Spring Data, JPA, Hibernate to work with Database
- How to define Data Models and Repository interfaces for JPA Many-To-Many relationship
- Way to use Spring JPA to interact with Database for Many-To-Many association
- Way to create Spring Rest Controller to process HTTP requests
Related Posts:
– JPA One To One example with Hibernate and Spring Boot
– JPA One To Many example with Hibernate and Spring Boot
– Validate Request Body in Spring Boot
– Spring Boot Token based Authentication with Spring Security & JWT
– Spring JPA + H2 example
– Spring JPA + MySQL example
– Spring JPA + PostgreSQL example
– Spring JPA + Oracle example
– Spring JPA + SQL Server example
– Documentation: Spring Boot Swagger 3 example
– Caching: Spring Boot Redis Cache example
Contents
JPA Many To Many example
We’re gonna create a Spring project from scratch, then we implement JPA/Hibernate Many to Many Mapping with tutorials
and tags
table as following:
tutorial_tags
contains tutorial_id
and associated tag_id
.
We also write Rest Apis to perform CRUD operations on the Tutorial and Tag entities.
These are APIs that we need to provide:
Methods | Urls | Actions |
---|---|---|
POST | /api/tutorials/:id/tags | create/add Tag for a Tutorial |
GET | /api/tutorials/:id/tags | retrieve all Tags of a Tutorial |
GET | /api/tags/:id/tutorials | retrieve all Tutorials of a Tag |
GET | /api/tutorials | retrieve all Tutorials |
GET | /api/tutorials/:id | retrieve a Tutorial with it Tags |
GET | /api/tags | retrieve all Tags |
GET | /api/tags/:id | retrieve a Tag by :id |
PUT | /api/tags/:id | update a Tag by :id |
DELETE | /api/tags/:id | delete a Tag by :id |
DELETE | /api/tutorials/:id | delete a Tutorial by :id |
DELETE | /api/tutorials/:id/tags/:id | delete a Tag from a Tutorial by :id |
Here are the example requests.
– Create some Tutorials first:
Now we have tutorials table like this:
– Create a new Tag, assign to a Tutorial: POST /api/tutorials/[:id]/tags
Continue to create tag Java
for Tutorial with id=1, then we check the database:
- tags table
- tutorial_tags table
– Add an existing Tag to a Tutorial:
Check tutorial_tags table:
– Retrieve all Tags of a Tutorial: GET /api/tutorials/[:id]/tags
– Retrieve all Tutorials of a Tag: GET /api/tags/[:id]/tutorials
– Retrieve all Tutorials: GET /api/tutorials
– Delete a Tag from a Tutorial: DELETE /api/tutorials/[:id]/tags/[:id]
Check tutorial_tags table:
– Retrieve a Tutorial with its Tags: GET /api/tutorials/[:id]
You can see that the tag Java
was removed from it.
Let’s build our Spring Boot Many to Many CRUD example.
Spring Boot Many to Many example
Technology
- Java 8
- Spring Boot 2.6.2 (with Spring Web MVC, Spring Data JPA)
- H2/MySQL/PostgreSQL
- Maven 3.8.1
Project Structure
Let me explain it briefly.
– Tutorial
, Tag
data model class correspond to entity and table tutorials, tags.
– TutorialRepository
, TagRepository
are interfaces that extends JpaRepository for CRUD methods and custom finder methods. It will be autowired in TutorialController
, TagController
.
– TutorialController
, TagController
are RestControllers which has request mapping methods for RESTful CRUD API requests.
– Configuration for Spring Datasource, JPA & Hibernate in application.properties.
– pom.xml contains dependencies for Spring Boot and MySQL/PostgreSQL/H2 database.
– About exception package, to keep this post straightforward, I won’t explain it. For more details, you can read following tutorial:
@RestControllerAdvice example in Spring Boot
Create & Setup Spring Boot project
Use Spring web tool or your development tool (Spring Tool Suite, Eclipse, Intellij) to create a Spring Boot project.
Then open pom.xml and add these 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>
We also need to add one more dependency.
– If you want to use MySQL:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
– or PostgreSQL:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
– or H2 (embedded database):
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Configure Spring Datasource, JPA, Hibernate
Under src/main/resources folder, open application.properties and write these lines.
– For MySQL:
spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username= root
spring.datasource.password= 123456
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto= update
– For PostgreSQL:
spring.datasource.url= jdbc:postgresql://localhost:5432/testdb
spring.datasource.username= postgres
spring.datasource.password= 123
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation= true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto= update
spring.datasource.username
&spring.datasource.password
properties are the same as your database installation.- Spring Boot uses Hibernate for JPA implementation, we configure
MySQL5InnoDBDialect
for MySQL orPostgreSQLDialect
for PostgreSQL spring.jpa.hibernate.ddl-auto
is used for database initialization. We set the value toupdate
value so that a table will be created in the database automatically corresponding to defined data model. Any change to the model will also trigger an update to the table. For production, this property should bevalidate
.
– For H2 database:
spring.datasource.url=jdbc:h2:mem:testdb
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
# default path: h2-console
spring.h2.console.path=/h2-ui
spring.datasource.url
:jdbc:h2:mem:[database-name]
for In-memory database andjdbc:h2:file:[path/database-name]
for disk-based database.- We configure
H2Dialect
for H2 Database spring.h2.console.enabled=true
tells the Spring to start H2 Database administration tool and you can access this tool on the browser:http://localhost:8080/h2-console
.spring.h2.console.path=/h2-ui
is for H2 console’s url, so the default urlhttp://localhost:8080/h2-console
will change tohttp://localhost:8080/h2-ui
.
Define Data Model for JPA Many to Many mapping
In model package, we define Tutorial
and Tag
class.
Tutorial has four fields: id, title, description, published.
model/Tutorial.java
package com.bezkoder.spring.hibernate.manytomany.model;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
// import jakarta.persistence.*; // for Spring Boot 3
@Entity
@Table(name = "tutorials")
public class Tutorial {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "title")
private String title;
@Column(name = "description")
private String description;
@Column(name = "published")
private boolean published;
@ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name = "tutorial_tags",
joinColumns = { @JoinColumn(name = "tutorial_id") },
inverseJoinColumns = { @JoinColumn(name = "tag_id") })
private Set<Tag> tags = new HashSet<>();
public Tutorial() {
}
public Tutorial(String title, String description, boolean published) {
this.title = title;
this.description = description;
this.published = published;
}
// getters and setters
public void addTag(Tag tag) {
this.tags.add(tag);
tag.getTutorials().add(this);
}
public void removeTag(long tagId) {
Tag tag = this.tags.stream().filter(t -> t.getId() == tagId).findFirst().orElse(null);
if (tag != null) {
this.tags.remove(tag);
tag.getTutorials().remove(this);
}
}
}
– @Entity
annotation indicates that the class is a persistent Java class.
– @Table
annotation provides the table that maps this entity.
– @Id
annotation is for the primary key.
– @GeneratedValue
annotation is used to define generation strategy for the primary key.
– @Column
annotation is used to define the column in database that maps annotated field.
model/Tag.java
package com.bezkoder.spring.hibernate.manytomany.model;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
// import jakarta.persistence.*; // for Spring Boot 3
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = "tags")
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name")
private String name;
@ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
mappedBy = "tags")
@JsonIgnore
private Set<Tutorial> tutorials = new HashSet<>();
public Tag() {
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Tutorial> getTutorials() {
return tutorials;
}
public void setTutorials(Set<Tutorial> tutorials) {
this.tutorials = tutorials;
}
}
We use @ManyToMany
annotation for Many-to-Many association between two entities: Tutorial and Tag.
Every Many-to-Many relationship has two sides:
- the owning side
- the non-owning (inverse side)
In this tutorial, Tutorial entity is the owner of the relationship and Tag entity is the inverse side.
The join table is specified on the owning side (Tutorial) using @JoinTable
annotation. This relationship is bidirectional, the inverse side (Tag) must use the mappedBy
element to specify the relationship field or property of the owning side.
So, the side which doesn’t have the mappedBy
attribute is the owner, the side which has the mappedBy
attribute is the inverse side.
The owner side is the side which Hibernate looks at to know which association exists. For example, if you add a Tag in the set of tags
of a Tutorial, a new row will be inserted by Hibernate in the join table (tutorial_tags). On the contrary, if you add a Tutorial to the set of tutorials
of a Tag, nothing will be modified in the database.
@JsonIgnore
is used to ignore the logical property used in serialization and deserialization.
Create Repository Interfaces for Many To Many mapping
Let’s create a repository to interact with database.
In repository package, create TutorialRepository
and TagRepository
interfaces that extend JpaRepository
.
repository/TutorialRepository.java
package com.bezkoder.spring.hibernate.manytomany.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bezkoder.spring.hibernate.manytomany.model.Tutorial;
public interface TutorialRepository extends JpaRepository<Tutorial, Long> {
// ...
List<Tutorial> findTutorialsByTagsId(Long tagId);
}
repository/TagRepository.java
package com.bezkoder.spring.hibernate.manytomany.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bezkoder.spring.hibernate.manytomany.model.Tag;
public interface TagRepository extends JpaRepository<Tag, Long> {
List<Tag> findTagsByTutorialsId(Long tutorialId);
}
Now we can use JpaRepository’s methods: save()
, findOne()
, findById()
, findAll()
, count()
, delete()
, deleteById()
… without implementing these methods.
We also define custom finder methods:
findTutorialsByTagsId()
: returns all Tutorials related to Tag with inputtagId
.findTagsByTutorialsId()
: returns all Tags related to Tutorial with inputtutorialId
.
The implementation is plugged in by Spring Data JPA automatically.
More Derived queries at:
JPA Repository query example in Spring Boot
Custom query with @Query
annotation:
Spring JPA @Query example: Custom query in Spring Boot
You also find way to write Unit Test for this JPA Repository at:
Spring Boot Unit Test for JPA Repository with @DataJpaTest
Create Spring Rest APIs Controller
Finally, we create controller that provides APIs for CRUD operations: creating, retrieving, updating, deleting and finding Tutorials and Tags.
controller/TutorialController.java
package com.bezkoder.spring.hibernate.manytomany.controller;
import java.util.ArrayList;
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.*;
import com.bezkoder.spring.hibernate.manytomany.exception.ResourceNotFoundException;
import com.bezkoder.spring.hibernate.manytomany.model.Tutorial;
import com.bezkoder.spring.hibernate.manytomany.repository.TutorialRepository;
@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class TutorialController {
@Autowired
TutorialRepository tutorialRepository;
@GetMapping("/tutorials")
public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) {
List<Tutorial> tutorials = new ArrayList<Tutorial>();
if (title == null)
tutorialRepository.findAll().forEach(tutorials::add);
else
tutorialRepository.findByTitleContaining(title).forEach(tutorials::add);
if (tutorials.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(tutorials, HttpStatus.OK);
}
@GetMapping("/tutorials/{id}")
public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) {
Tutorial tutorial = tutorialRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));
return new ResponseEntity<>(tutorial, HttpStatus.OK);
}
@PostMapping("/tutorials")
public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) {
Tutorial _tutorial = tutorialRepository.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), true));
return new ResponseEntity<>(_tutorial, HttpStatus.CREATED);
}
@PutMapping("/tutorials/{id}")
public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
Tutorial _tutorial = tutorialRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));
_tutorial.setTitle(tutorial.getTitle());
_tutorial.setDescription(tutorial.getDescription());
_tutorial.setPublished(tutorial.isPublished());
return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK);
}
@DeleteMapping("/tutorials/{id}")
public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") long id) {
tutorialRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@DeleteMapping("/tutorials")
public ResponseEntity<HttpStatus> deleteAllTutorials() {
tutorialRepository.deleteAll();
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@GetMapping("/tutorials/published")
public ResponseEntity<List<Tutorial>> findByPublished() {
List<Tutorial> tutorials = tutorialRepository.findByPublished(true);
if (tutorials.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(tutorials, HttpStatus.OK);
}
}
controller/TagController.java
package com.bezkoder.spring.hibernate.manytomany.controller;
import java.util.ArrayList;
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.*;
import com.bezkoder.spring.hibernate.manytomany.exception.ResourceNotFoundException;
import com.bezkoder.spring.hibernate.manytomany.model.Tag;
import com.bezkoder.spring.hibernate.manytomany.model.Tutorial;
import com.bezkoder.spring.hibernate.manytomany.repository.TagRepository;
import com.bezkoder.spring.hibernate.manytomany.repository.TutorialRepository;
@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class TagController {
@Autowired
private TutorialRepository tutorialRepository;
@Autowired
private TagRepository tagRepository;
@GetMapping("/tags")
public ResponseEntity<List<Tag>> getAllTags() {
List<Tag> tags = new ArrayList<Tag>();
tagRepository.findAll().forEach(tags::add);
if (tags.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(tags, HttpStatus.OK);
}
@GetMapping("/tutorials/{tutorialId}/tags")
public ResponseEntity<List<Tag>> getAllTagsByTutorialId(@PathVariable(value = "tutorialId") Long tutorialId) {
if (!tutorialRepository.existsById(tutorialId)) {
throw new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId);
}
List<Tag> tags = tagRepository.findTagsByTutorialsId(tutorialId);
return new ResponseEntity<>(tags, HttpStatus.OK);
}
@GetMapping("/tags/{id}")
public ResponseEntity<Tag> getTagsById(@PathVariable(value = "id") Long id) {
Tag tag = tagRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tag with id = " + id));
return new ResponseEntity<>(tag, HttpStatus.OK);
}
@GetMapping("/tags/{tagId}/tutorials")
public ResponseEntity<List<Tutorial>> getAllTutorialsByTagId(@PathVariable(value = "tagId") Long tagId) {
if (!tagRepository.existsById(tagId)) {
throw new ResourceNotFoundException("Not found Tag with id = " + tagId);
}
List<Tutorial> tutorials = tutorialRepository.findTutorialsByTagsId(tagId);
return new ResponseEntity<>(tutorials, HttpStatus.OK);
}
@PostMapping("/tutorials/{tutorialId}/tags")
public ResponseEntity<Tag> addTag(@PathVariable(value = "tutorialId") Long tutorialId, @RequestBody Tag tagRequest) {
Tag tag = tutorialRepository.findById(tutorialId).map(tutorial -> {
long tagId = tagRequest.getId();
// tag is existed
if (tagId != 0L) {
Tag _tag = tagRepository.findById(tagId)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tag with id = " + tagId));
tutorial.addTag(_tag);
tutorialRepository.save(tutorial);
return _tag;
}
// add and create new Tag
tutorial.addTag(tagRequest);
return tagRepository.save(tagRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));
return new ResponseEntity<>(tag, HttpStatus.CREATED);
}
@PutMapping("/tags/{id}")
public ResponseEntity<Tag> updateTag(@PathVariable("id") long id, @RequestBody Tag tagRequest) {
Tag tag = tagRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("TagId " + id + "not found"));
tag.setName(tagRequest.getName());
return new ResponseEntity<>(tagRepository.save(tag), HttpStatus.OK);
}
@DeleteMapping("/tutorials/{tutorialId}/tags/{tagId}")
public ResponseEntity<HttpStatus> deleteTagFromTutorial(@PathVariable(value = "tutorialId") Long tutorialId, @PathVariable(value = "tagId") Long tagId) {
Tutorial tutorial = tutorialRepository.findById(tutorialId)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));
tutorial.removeTag(tagId);
tutorialRepository.save(tutorial);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@DeleteMapping("/tags/{id}")
public ResponseEntity<HttpStatus> deleteTag(@PathVariable("id") long id) {
tagRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
Conclusion
Today we’ve built a Spring Boot CRUD example (Rest API) using Spring Data JPA, Hibernate Many to Many relationship with MySQL/PostgreSQL/embedded database (H2).
We also see way for implementing JPA Many to Many Mapping, and JpaRepository
supports a great way to make CRUD operations, custom finder methods without need of boilerplate code.
Custom query with @Query
annotation:
Spring JPA @Query example: Custom query in Spring Boot
If you want to add Pagination to this Spring project, you can find the instruction at:
Spring Boot Pagination & Filter example | Spring JPA, Pageable
To sort/order by multiple fields:
Spring Data JPA Sort/Order by multiple Columns | Spring Boot
Handle Exception for this Rest APIs is necessary:
– Spring Boot @ControllerAdvice & @ExceptionHandler example
– @RestControllerAdvice example in Spring Boot
Or way to write Unit Test for the JPA Repository:
Spring Boot Unit Test for JPA Repository with @DataJpaTest
You can also know:
– Validate Request Body in Spring Boot
– how to deploy this Spring Boot App on AWS (for free) with this tutorial.
– dockerize with Docker Compose: Spring Boot and MySQL example
– way to upload an Excel file and store the data in MySQL database with this post
– upload CSV file and store the data in MySQL with this post.
Happy learning! See you again.
Further Reading
- Secure Spring Boot App with Spring Security & JWT Authentication
- Spring Data JPA Reference Documentation
- Spring Boot Pagination and Sorting example
Fullstack CRUD App:
– Vue + Spring Boot example
– Angular 8 + Spring Boot example
– Angular 10 + Spring Boot example
– Angular 11 + Spring Boot example
– Angular 12 + Spring Boot example
– Angular 13 + Spring Boot example
– Angular 14 + Spring Boot example
– React + Spring Boot example
Source Code
You can find the complete source code for this tutorial on Github.
One-to-Many: JPA One To Many example with Hibernate and Spring Boot
One-to-One: JPA One To One example with Hibernate and Spring Boot
You can apply this implementation in following tutorials:
– Spring JPA + H2 example
– Spring JPA + MySQL example
– Spring JPA + PostgreSQL example
– Spring JPA + Oracle example
– Spring JPA + SQL Server example
More Derived queries at:
JPA Repository query example in Spring Boot
Documentation: Spring Boot + Swagger 3 example (with OpenAPI 3)
Caching: Spring Boot Redis Cache example
Where does findByTitleContaining come from? Any method like that from ORM?
Hi, you can read following tutorial for more details:
JPA Repository query example in Spring Boot | Derived Query
Hello,
This is a super good example.
I was struggling with the implementation for many-to-many.
My doubts were clarified from your example. I am super thankful to you.
Hi There,
Thank you very much for your time in sharing knowledge to others, it is really good content .
Just a quick question: you have specified fetch strategy as lazy , I am wondering why tags displaying when we are reading all tutorials.
I hope that you post the front End side in quick time