JPA and Hibernate provide @ManyToOne and @OneToMany as the two primary annotations for mapping One to Many unidirectional and bidirectional relationship
A bidirectional relationship provides navigation access to both sides while a unidirectional relationship provides navigation access to one side only
This tutorial will walk you through the steps of using @OneToMany and @ManyToOne to do a bidirectional mapping for a JPA and Hibernate One to Many relationship, and writing CRUD REST APIs to expose the relationship for accessing the database in Spring Boot, Spring Data JPA, and MySQL
There are a convenient benefit and also a performance tradeoff when using @OneToMany. We will address them and find the approach
Let's get started!
What you'll need
- JDK 8+ or OpenJDK 8+
- Maven 3+
- MySQL Server 5+
- Your favorite IDE
Project structure
Following is the final project structure with all the files we would create
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── hellokoding
│ │ └── jpa
│ │ └── bidirectional
│ │ ├── Application.java
│ │ ├── Book.java
│ │ ├── BookController.java
│ │ ├── BookRepository.java
│ │ ├── Library.java
│ │ ├── LibraryController.java
│ │ └── LibraryRepository.java
│ └── resources
│ └── application.properties
└── pom.xml
Create a sample Spring Boot application
Create a new Spring Boot application with Spring Initializr via web UI or a command-line tool such as cURL or HTTPie, you can find the guide at here
Example with the cURL command-line
curl https://start.spring.io/starter.zip \
-d dependencies=jpa,mysql,web \
-d javaVersion=1.8 \
-d packageName=com.hellokoding.jpa \
-d groupId=com.hellokoding.jpa \
-o hk-demo-jpa.zip
Unzip the hk-demo-jpa.zip file and import the sample project into your IDE
Project dependencies
We will need the following dependencies on the pom.xml file
spring-boot-starter-data-jpa
to work with JPA and Hibernatemysql-connector-java
to work with MySQL. The scope runtime indicates that the dependency is not required for compilation, but for executionspring-boot-starter-web
for defining the CRUD REST APIs for the one-to-many relationship mapping
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
The One to Many Relationship
One-to-many refers to the relationship between two tables A and B in which one row of A may be linked with many rows of B, but one row of B is linked to only one row of A
We will use the relationship between the library and books to implement for this example. One library may have many books, one book can only be managed by one library. The relationship is enforced via the library_id
foreign key column placed on the book
table (the Many side)
Define JPA and Hibernate Entities
Create Library
and Book
JPA Entities corresponding to library
and book
tables in the database.
package com.hellokoding.jpa.bidirectional;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.HashSet;
import java.util.Set;
@Entity
public class Library {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@NotNull
private String name;
@OneToMany(mappedBy = "library", cascade = CascadeType.ALL)
private Set<Book> books = new HashSet<>();
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Book> getBooks() {
return books;
}
public void setBooks(Set<Book> books) {
this.books = books;
for(Book b : books) {
b.setLibrary(this);
}
}
}
package com.hellokoding.jpa.bidirectional;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@NotNull
private String name;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "library_id")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private Library library;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Library getLibrary() {
return library;
}
public void setLibrary(Library library) {
this.library = library;
}
}
@Entity annotation is required to specify a JPA and Hibernate entity
@Id annotation is required to specify the identifier property of the entity
@OneToMany and @ManyToOne defines a one-to-many and many-to-one relationship between 2 entities. @JoinColumn specifies the foreign key column. mappedBy indicates the entity is the inverse of the relationship
@OneToMany should be placed on the parent entity (the One side), and @ManyToOne should be placed on the child entity (the Many side)
The default fetchType of @ManyToOne is EAGER which can cause performance issue, so here we change it to LAZY
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) is for the REST APIs section below to ignore the property when serializing it to JSON string, due to library
is a LAZY association which can throw LazyInitializationException if it is uninitialized in a non-transactional context
CascadeType.ALL is for propagating the CRUD operations on the parent entity to the child entities. CascadeType.ALL should be used for small child collection only as it can cause performance issue, we will dig more into this in the later part
Extend Spring Data JPA Repository Interfaces
Spring Data JPA provides a collection of repository interfaces that help reducing boilerplate code required to implement the data access layer for various databases
Let's create LibraryRepository and BookRepository interfaces, then extend them from JpaRepository
package com.hellokoding.jpa.library;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LibraryRepository extends JpaRepository<Library, Integer>{
}
package com.hellokoding.jpa.unidirectional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import javax.transaction.Transactional;
public interface BookRepository extends JpaRepository<Book, Integer>{
}
Create the One-to-Many CRUD REST APIs to access database
Create LibraryController and BookController to define CRUD REST APIs for accessing the database via the one to many relationship mapping
package com.hellokoding.jpa.bidirectional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;
import java.util.Optional;
@RestController
@RequestMapping("/api/v1/libraries")
public class LibraryController {
private final LibraryRepository libraryRepository;
private final BookRepository bookRepository;
@Autowired
public LibraryController(LibraryRepository libraryRepository, BookRepository bookRepository) {
this.libraryRepository = libraryRepository;
this.bookRepository = bookRepository;
}
@PostMapping
public ResponseEntity<Library> create(@Valid @RequestBody Library library) {
Library savedLibrary = libraryRepository.save(library);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedLibrary.getId()).toUri();
return ResponseEntity.created(location).body(savedLibrary);
}
@PutMapping("/{id}")
public ResponseEntity<Library> update(@PathVariable Integer id, @Valid @RequestBody Library library) {
Optional<Library> optionalLibrary = libraryRepository.findById(id);
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
library.setId(optionalLibrary.get().getId());
libraryRepository.save(library);
return ResponseEntity.noContent().build();
}
@DeleteMapping("/{id}")
public ResponseEntity<Library> delete(@PathVariable Integer id) {
Optional<Library> optionalLibrary = libraryRepository.findById(id);
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
libraryRepository.delete(optionalLibrary.get());
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}")
public ResponseEntity<Library> getById(@PathVariable Integer id) {
Optional<Library> optionalLibrary = libraryRepository.findById(id);
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
return ResponseEntity.ok(optionalLibrary.get());
}
@GetMapping
public ResponseEntity<Page<Library>> getAll(Pageable pageable) {
return ResponseEntity.ok(libraryRepository.findAll(pageable));
}
}
package com.hellokoding.jpa.bidirectional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;
import java.util.Optional;
@RestController
@RequestMapping("/api/v1/books")
public class BookController {
private final BookRepository bookRepository;
private final LibraryRepository libraryRepository;
@Autowired
public BookController(BookRepository bookRepository, LibraryRepository libraryRepository) {
this.bookRepository = bookRepository;
this.libraryRepository = libraryRepository;
}
@PostMapping
public ResponseEntity<Book> create(@RequestBody @Valid Book book) {
Optional<Library> optionalLibrary = libraryRepository.findById(book.getLibrary().getId());
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
book.setLibrary(optionalLibrary.get());
Book savedBook = bookRepository.save(book);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedBook.getId()).toUri();
return ResponseEntity.created(location).body(savedBook);
}
@PutMapping("/{id}")
public ResponseEntity<Book> update(@RequestBody @Valid Book book, @PathVariable Integer id) {
Optional<Library> optionalLibrary = libraryRepository.findById(book.getLibrary().getId());
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
Optional<Book> optionalBook = bookRepository.findById(id);
if (!optionalBook.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
book.setLibrary(optionalLibrary.get());
book.setId(optionalBook.get().getId());
bookRepository.save(book);
return ResponseEntity.noContent().build();
}
@DeleteMapping("/{id}")
public ResponseEntity<Book> delete(@PathVariable Integer id) {
Optional<Book> optionalBook = bookRepository.findById(id);
if (!optionalBook.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
bookRepository.delete(optionalBook.get());
return ResponseEntity.noContent().build();
}
@GetMapping
public ResponseEntity<Page<Book>> getAll(Pageable pageable) {
return ResponseEntity.ok(bookRepository.findAll(pageable));
}
@GetMapping("/{id}")
public ResponseEntity<Book> getById(@PathVariable Integer id) {
Optional<Book> optionalBook = bookRepository.findById(id);
if (!optionalBook.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
return ResponseEntity.ok(optionalBook.get());
}
}
Application Configurations
Configure the Spring Datasource JDBC URL, user name, and password of your local MySQL server in application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=hellokoding
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
Create the test database in your local MySQL server if not exists
We don't have to create table schemas, the ddl-auto=create
config allows JPA and
Hibernate does that based on the entity-relationship mappings. In practice, consider to use ddl-auto=none
(default) and use a migration tool such as Flyway for better database management
spring.jpa.show-sql=true
for showing generated SQL queries in the application logs, consider to disable it on production environment
Run the application
We use @SpringBootApplication to launch the application
package com.hellokoding.jpa.bidirectional;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Type the mvn command at the project root directory to start the application
./mvnw clean spring-boot:run -Dstart-class=com.hellokoding.jpa.bidirectional.Application
Access to your local MySQL Server to query the table schemas created by JPA and Hibernate based on your entity mapping
The pros of @OneToMany
Let's test the one to many REST APIs with Postman
Thanks to the CascadeType.ALL setting with @OneToMany mapping on the parent entity
@OneToMany(mappedBy = "library", cascade = CascadeType.ALL)
private Set<Book> books = new HashSet<>();
You can cascade the CRUD operations on the parent to child collection by using a single line of code
1) Create a list of new child entities when creating a new parent via libraryRepository.save(library);
in the Create Library API
@PostMapping
public ResponseEntity<Library> create(@Valid @RequestBody Library library) {
Library savedLibrary = libraryRepository.save(library);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedLibrary.getId()).toUri();
return ResponseEntity.created(location).body(savedLibrary);
}
2) Create a list of new child entities when updating an existing parent via libraryRepository.save(library);
in the Update Library API
@PutMapping("/{id}")
public ResponseEntity<Library> update(@PathVariable Integer id, @Valid @RequestBody Library library) {
Optional<Library> optionalLibrary = libraryRepository.findById(id);
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
library.setId(optionalLibrary.get().getId());
libraryRepository.save(library);
return ResponseEntity.noContent().build();
}
3) Retrieve a list of child entities when retrieving a parent entity via libraryRepository.findById(id);
in the Get Library API
@GetMapping("/{id}")
public ResponseEntity<Library> getById(@PathVariable Integer id) {
Optional<Library> optionalLibrary = libraryRepository.findById(id);
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
return ResponseEntity.ok(optionalLibrary.get());
}
4) Delete a library by ID and all books belong to it via libraryRepository.delete(optionalLibrary.get());
in the Delete API
@DeleteMapping("/{id}")
public ResponseEntity<Library> delete(@PathVariable Integer id) {
Optional<Library> optionalLibrary = libraryRepository.findById(id);
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
libraryRepository.delete(optionalLibrary.get());
return ResponseEntity.noContent().build();
}
The cons of @OneToMany
Everything is a tradeoff, and indeed the @OneToMany sugar syntax comes with performance issues
1) It is not possible to limit the size of the @OneToMany collection directly from the database. So all the APIs that retrieving the @OneToMany collection may hit a performance issue when the collection size grows
2) Using CascadeType.ALL and CascadeType.REMOVE with @OneToMany can cause a performance issue
The above Delete API works fine on the surface, but if look into the console log, we would see the following
Hibernate: select library0_.id as id1_1_0_, library0_.name as name2_1_0_ from library library0_ where library0_.id=?
Hibernate: select books0_.library_id as library_3_0_0_, books0_.id as id1_0_0_, books0_.id as id1_0_1_, books0_.library_id as library_3_0_1_, books0_.name as name2_0_1_ from book books0_ where books0_.library_id=?
Hibernate: delete from book where id=?
Hibernate: delete from book where id=?
Hibernate: delete from library where id=?
To do the REMOVE cascading operations, Hibernate has to generate 1 SQL to get all child entities and N+1 DELETE queries. Obviously, to delete the child collection, we only need 1 DELETE query and we don't have to fetch the entire collection
A compromise approach for @OneToMany
We can fix the performance issues of @OneToMany, but have to trade the convenient cascading and navigating operations
1) Update @OneToMany settings to use cascade = {CascadeType.PERSIST,CascadeType.MERGE} instead of CascadeType.ALL, and remove the getChildCollection()
method from the parent entity
@Entity
public class Library {
...
@OneToMany(mappedBy = "library", cascade = {CascadeType.PERSIST,CascadeType.MERGE}
private Set<Book> books = new HashSet<>();
...
// public Set<Book> getBooks() {
// return books;
// }
}
2) Add custom delete and select queries into the BookRespository
public interface BookRepository extends JpaRepository<Book, Integer>{
Page<Book> findByLibraryId(Integer libraryId, Pageable pageable);
@Modifying
@Transactional
@Query("DELETE FROM Book b WHERE b.library.id = ?1")
void deleteByLibraryId(Integer libraryId);
}
3) Update LibraryController to use the new methods from BookRepository
public class LibraryController {
...
@DeleteMapping("/{id}")
public ResponseEntity<Library> delete(@PathVariable Integer id) {
Optional<Library> optionalLibrary = libraryRepository.findById(id);
if (!optionalLibrary.isPresent()) {
return ResponseEntity.unprocessableEntity().build();
}
deleteLibraryInTransaction(optionalLibrary.get());
return ResponseEntity.noContent().build();
}
@Transactional
public void deleteLibraryInTransaction(Library library) {
bookRepository.deleteByLibraryId(library.getId());
libraryRepository.delete(library);
}
...
}
4) Add a new API into BookController for retrieving the collection association by a parent entity id
public class BookController {
...
@GetMapping("/library/{libraryId}")
public ResponseEntity<Page<Book>> getByLibraryId(@PathVariable Integer libraryId, Pageable pageable) {
return ResponseEntity.ok(bookRepository.findByLibraryId(libraryId, pageable));
}
...
}
Restart the application and test the Delete Lirary API with Postman again you would see that to delete the child collection, Hibernate only generates 1 DELETE query
Hibernate: delete from book where library_id=?
As we remove the getBooks() method from the Library entity, the create
, update
, and getById
APIs won't return the list books anymore but we can fetch it via the getByLibraryId
API or if you would like to include it in the response of those APIs, consider to use the DTO design pattern like the suggestion in the later part
When to use @OneToMany
In summary, you can use @OneToMany if the child collection size is limited, otherwise, if the child collection can grow to a lot of items, consider to
Don't retrieve @OneToMany child collection directly from the parent, you can retrieve via custom queries on the repositories like the step 2 above
Don't use CascadeType.REMOVE or CascadeType.ALL with @OneToMany
Unidirectional mapping with the only @ManyToOne
You can use the only @ManyToOne to do the mapping for the One to Many unidirectional relationship. You may have to do more with the only @ManyToOne but it can help you worry less about the potential issues of @OneToMany
Convert JPA and Hibernate entities to DTO objects
In practice, one JPA and Hibernate entity model can not fit the various needs of clients. So instead of exposing directly, you may like converting JPA and Hibernate entities to DTO objects for providing custom API response data to the client
Conclusion
In this tutorial, we learned about bidirectional mapping the One-To-Many relationship with @OneToMany and @ManyToOne and expose it through REST APIs in Spring Boot and Spring Data JPA to do CRUD operations against a MySQL database. We also had a look at the pros and cons of using @OneToMany. The source code is available on Github
You may also like the other tutorials about entity relationship mapping in JPA and Hibernate