In relational databases (MySQL, PostgreSQL, etc.), One-to-many refers to the relationship between two tables (entities) A and B in which one row (element) of A may be linked with many rows of B, but one row of B is linked to only one row of A

In the below example, the book_category and book tables have a one-to-many relationship. One category may have many books but one book belongs to only one category

The relationship is enforced via the foreign key book_category_id which is a copy of the primary key of the table in the One side (book_category) and placed on the Many side (book). The table with the foreign key (book) is also known as the relationship owner

This tutorial will walk you through the steps of mapping that One-To-Many bidirectional entity relationships in JPA and Hibernate with Spring Boot, Spring Data JPA, MySQL and Docker

Let's get started

What you'll need

  • JDK 8+ or OpenJDK 8+
  • Maven 3+
  • MySQL Server 5+ or Docker CE 18+
  • Your favorite IDE

Tech stack

  • Java 8+
  • Spring Boot
  • Spring Data JPA
  • Hibernate

Init project structure

You can create and init a new Spring Boot project by using Spring CLI, Spring Initializr or your IDE. Learn more about using these tools here

The final project structure as below

├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── hellokoding
│       │           ├── jpa
│       │           │   ├── book
│       │           │   │   ├── Book.java
│       │           │   │   ├── BookCategory.java
│       │           │   │   ├── BookCategoryRepository.java
│       │           │   │   └── BookRepository.java
│       │           │   └── JpaApplication.java
│       │           └── springboot
│       └── resources
│           └── application.properties
├── Dockerfile
├── docker-compose.yml
└── pom.xml

Project dependencies

To use Spring Data JPA in Spring Boot, include spring-boot-starter-data-jpa dependency into your pom.xml or build.gradle file. The library version can be omitted as it is resolved by the parent pom provided by Spring Boot

<dependency>  
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>  

To work with MySQL database, a MySQL Java client library also need to be included as a dependency

<dependency>  
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>  

The scope runtime indicates that the dependency is not required for compilation, but is for execution. It is in the runtime and test classpaths, but not the compile classpath

You can find the full pom.xml file as below


Create JPA and Hibernate Entities

We'll create Book and BookCategory JPA Entities corresponding to book and book_category tables in database


@Entity annotation is required to specify a JPA entity. The name attribute is optional, defaults to the entity class name without the package. It is used to refer to the entity in JPQL (Java Persistence Query Language) queries

@Table annotation is optional and used to refer to the table in a database, defaults to the entity name

@Id annotation is required and used to specify the primary key of an entity (table in database). You will get an exception "No identifier specified for entity" if ignores it

@GeneratedValue annotation provides the generation strategies for the values of primary keys

@Column annotation is optional and used to map with the table column in the database, defaults to property or field name

@OneToMany is used to specify the One-To-Many relationship association with another entity which is annotated with @ManyToOne

  • mappedBy is required in a bidirectional relationship to specify the field or property name of the owner entity of the relationship

  • cascade is optional and used to specify which entity operations should be cascaded (propagated) to the associated entity, defaults to no operations. cascade=ALL is equivalent to cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}

  • fetch is optional and used to specify the strategy for the persistence provider runtime (Hibernate) to fetch data from the database

    There are two fetching strategies EAGER and LAZY, defaults to LAZY in ToMany associations (@OneToMany, @ManyToMany). When fetching data for an entity, JPA and Hibernate will also fetch data for EAGER associations while the LAZY will be fetched on-demand


@ManyToOne is used to specify a single-value relationship association to another entity which annotated with @OneToMany

  • fetch is optional and defaults to EAGER in ToOne associations (@ManyToOne, @OneToOne)

    The EAGER strategy should be avoided in practice, as it generates and executes unnecessary SQL scripts thus adds more weight load to the underlying database

@JoinColumn is used to specify the foreign key column in the underlying database table. In single join column, it is optional and the default attribute value will be used

  • name is defaulted to the property or field name joins with an underscore character and the primary key name of the reference entity

  • referencedColumnName is defaulted to the primary key of the preferenced table

Extend Spring Data JPA Repository Interfaces

Spring Data Repositories is a collection of interfaces help reducing boilerplate code required to implement the data access layer for various databases. They provide CRUD functions based on EntityManager to work with database

Internally, SimpleJpaRepository is the implementation of these interfaces in Spring Data JPA



Besides using the default implementation methods, you can also define custom CRUD operations via method declarations in your repository interfaces

In BookRepository, the findFirst10ByOrderByNameAsc method will do as its name, sort the book table in ascending order and get only the first 10 rows

  • findFirst10ByOrderByNameAsc can cause the N+1 SQL statements problem, the @EntityGraph(attributePaths = "bookCategory") annotation is used to fix it

The deleteInBulkByCategoryId and deleteByCategoryId will delete all books with the category id as the input parameter categoryId

  • While deleteInBulkByCategoryId only issues a single SQL query against the database, deleteByCategoryId issues N+1 SQL to delete one by one and track the life cycle of JPA entities

Learn more about the N+1 SQL statements problem

Create, read and delete data

We are done with mapping the entity-relationship and defining data access functions so far. Let's put them in action to see how to use them to do CRUD operations against the underlying database




In the create() method, the associated collections of Book will be persisted to database along with BookCategory when the save function of BookCategoryRepository is triggered without the need of calling BookRepository save function explicitly

bookCategoryRepository.saveAll(bookCategories);  

The above works thanks to the following configurations

  • cascade = CascadeType.PERSIST of @OneToMany annotation in BookCategory class

  • setBookCategory method in Book class

public void setBookCategory(BookCategory bookCategory) {  
    this.bookCategory = bookCategory;
    bookCategory.getBooks().add(this);
}



We don't have to annotate create(), read() and delete() methods with @Transactional

  • In the create() method, the saveAll method is already marked with @Transactional in the base repository implementation SimpleJpaRepository

  • In the read() method, only 1 SQL is generated by Hibernate (thus no N+1 SQL and LazyInitializationException issues) thanks to @EntityGraph(attributePaths = "bookCategory") defined in BookRepository

  • In the delete() method, the deleteInBulkByCategoryId method is already marked with @Transactional in BookRepository

Learn more about @Transactional, LazyInitializationException problem, and N+1 SQL statements problem

Application Configurations


hk-mysql refers to Docker Compose service defined in the below docker-compose.yml file

spring.jpa.hibernate.ddl-auto=create allows JPA/Hibernate auto-create database and table schema for you. In practice, you may like to disable the DDL Auto feature by using spring.jpa.hibernate.ddl-auto=validate or spring.jpa.hibernate.ddl-auto=none (default). Check out this example as one of the approaches Database Migration/Evolution Example with Flyway and JPA/Hibernate

These properties are for the development environment, thus they should be disabled on production

spring.jpa.show-sql=true  
spring.jpa.properties.hibernate.generate_statistics=true  

Run with Docker

Prepare Dockerfile for Java/Spring Boot application and docker-compose.yml for MySQL Server



Type the below command at the project root directory, make sure your local Docker is running

docker-compose up  

Access to MySQL Server docker container by issuing below bash command and key in hellokoding on Enter password:

docker exec -it hk-mysql mysql -p

Query schema and data created by JPA/Hibernate based on your mapping

Run with JDK/OpenJDK, Maven and MySQL Server local

Update hk-mysql on application.properties to localhost and type the below command at the project root directory

mvn clean spring-boot:run  

Conclusion

In this tutorial, we learn about the One-To-Many relationship, how to map and use it in Spring Boot and Spring Data JPA to do CRUD operations against a MySQL database. The full source code is available at here

You may also like the other tutorials about entity relationship mapping in JPA and Hibernate