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 Hibernate

  • mysql-connector-java to work with MySQL. The scope runtime indicates that the dependency is not required for compilation, but for execution

  • spring-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