JPA and Hibernate provide @ManyToOne and @OneToMany as the two primary annotations for mapping One to Many unidirectional and bidirectional relationship
The bidirectional relationship provides navigation access to both sides. The unidirectional relationship provides navigation access to one side only
While @OneToMany provides convenient access and cascading operations to the child collection, it has some tradeoffs about performance so it only works well on child collections with limited size. On the opposite, you may have to do more with using the only @ManyToOne but it can help you less worry about the potential issues
This tutorial will walk you through the steps of mapping a One to Many unidirectional relationship with @ManyToOne. We will also write CRUD REST APIs to expose the relationship for accessing the database in Spring Boot, Spring Data JPA, and MySQL
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
│ │ └── unidirectional
│ │ ├── 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 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.unidirectional;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
@Entity
public class Library {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@NotNull
private String name;
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;
}
}
package com.hellokoding.jpa.unidirectional;
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;
@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@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
@ManyToOne annotation is required to specify the one-to-many relationship between 2 entities. We should always declare it on the relationship owner entity (the Many side) which mapping to the table with the foreign key column in the underlying database
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
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>{
Page<Book> findByLibraryId(Integer libraryId, Pageable pageable);
@Modifying
@Transactional
@Query("DELETE FROM Book b WHERE b.library.id = ?1")
void deleteByLibraryId(Integer libraryId);
}
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.unidirectional;
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.transaction.Transactional;
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();
}
deleteLibraryInTransaction(optionalLibrary.get());
return ResponseEntity.noContent().build();
}
@Transactional
public void deleteLibraryInTransaction(Library library) {
bookRepository.deleteByLibraryId(library.getId());
libraryRepository.delete(library);
}
@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.unidirectional;
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());
}
@GetMapping("/library/{libraryId}")
public ResponseEntity<Page<Book>> getByLibraryId(@PathVariable Integer libraryId, Pageable pageable) {
return ResponseEntity.ok(bookRepository.findByLibraryId(libraryId, pageable));
}
}
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.unidirectional;
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.unidirectional.Application
Access to your local MySQL Server to query the table schemas created by JPA and Hibernate based on your entity mapping
Test the one to many REST APIs with Postman
Create a new library
Create a new book
Get a library by ID
Get a book by ID
Get all books by a library ID
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 mapping the One-To-Many unidirectional relationship with @ManyToOne and expose it through REST APIs in Spring Boot and Spring Data JPA to do CRUD operations against a MySQL database. The complete source code is available on Github