JPA/Hibernate One To Many Unidirectional example Spring Boot

In this tutorial, I will show you how to implement Spring JPA One-To-Many Unidirectional mapping with Hibernate in a Spring Boot example using @OneToMany 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 One-To-Many Unidirectional mapping
  • Way to use Spring JPA to interact with Database for One-To-Many Unidirectional association
  • Way to create Spring Rest Controller to process HTTP requests

Related Posts:
JPA One To One example
JPA Many to One example
JPA Many to Many example
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 (with OpenAPI 3)
– Caching: Spring Boot Redis Cache example

Ways to implement JPA/Hibernate One To Many mapping

In a relational database, a One-to-Many relationship between table A and table B indicates that one row in table A links to many rows in table B, but one row in table B links to only one row in table A.

For example, you need to design data model for a Tutorial Blog in which One Tutorial has Many Comments. So this is a One-to-Many association.

You can map the child entities as a collection (List of Comments) in the parent object (Tutorial), and JPA/Hibernate provides the @OneToMany annotation for that case: only the parent-side defines the relationship. We call it unidirectional @OneToMany association.

Similarly, when only the child-side manage the relationship, we have unidirectional Many-to-One association with @ManyToOne annotation where the child (Comment) has an entity object reference to its parent entity (Tutorial) by mapping the Foreign Key column (tutorial_id).

For more details about Many-to-One unidirectional, please visit:
JPA Many to One example

JPA One To Many Unidirectional example

We’re gonna create a Spring project from scratch, then we implement JPA/Hibernate One to Many Mapping with tutorials and comments table as following:

jpa-one-to-many-diagram

We also write Rest Apis to perform CRUD operations on the Comment entities.

These are APIs that we need to provide:

Methods Urls Actions
POST /api/tutorials/:id/comments create new Comment for a Tutorial
GET /api/tutorials/:id/comments retrieve all Comments of a Tutorial
GET /api/comments/:id retrieve a Comment by :id
PUT /api/comments/:id update a Comment by :id
DELETE /api/comments/:id delete a Comment by :id
DELETE /api/tutorials/:id delete a Tutorial (and its Comments) by :id
DELETE /api/tutorials/:id/comments delete all Comments of a Tutorial

Here are the example requests:

– Create new Tutorials: POST /api/tutorials/[:id]

jpa-one-to-many-unidirectional-example-create-tutorial

Assume that we’ve had tutorials table like this:

jpa-one-to-many-unidirectional-example-parent-entity-table

– Create new Comments: POST /api/tutorials/[:id]/comments

jpa-one-to-many-unidirectional-example-create-child-entity

comments table after that:

jpa-one-to-many-unidirectional-example-child-entity-table

– Retrieve a Tutorial with Comments array: GET /api/tutorials/[:id]

jpa-one-to-many-unidirectional-tutorial-get-one

– Retrieve all Comments of specific Tutorial: GET /api/tutorials/[:id]/comments

jpa-one-to-many-unidirectional-tutorial-get

– Delete all Comments of specific Tutorial: DELETE /api/tutorials/[:id]/comments

jpa-one-to-many-unidirectional-example-child-entity-delete

Check the comment table, all Comments of Tutorial with id=2 were deleted:

jpa-one-to-many-unidirectional-example-child-entity-delete-table

– Delete a Tutorial: DELETE /api/tutorials/[:id]

jpa-one-to-many-unidirectional-example-delete-cascade

jpa-one-to-many-unidirectional-example-tutorial-delete-cascade

All Comments of the Tutorial with id=3 were CASCADE deleted automatically.

jpa-one-to-many-unidirectional-example-delete-cascade-table

– Delete a Comment by id: DELETE /api/comments/[:id]

jpa-one-to-many-unidirectional-example-delete-one-child-entity

Check database:

jpa-one-to-many-unidirectional-example-delete-child-entity-database

– Retrieve all Tutorials after that: GET /api/tutorials

jpa-one-to-many-unidirectional-tutorial-get-all

Spring Boot One to Many Unidirectional example

Technology

  • Java 17 / 11 / 8
  • Spring Boot 3 / 2 (with Spring Web MVC, Spring Data JPA)
  • PostgreSQL/MySQL/H2 embedded database
  • Maven 3.8.1

Project Structure

spring-boot-jpa-one-to-many-unidirectional-example-project-structure

Let me explain it briefly.

Tutorial, Comment data model class correspond to entity and table tutorials, comments.
TutorialRepository, CommentRepository are interfaces that extends JpaRepository for CRUD methods and custom finder methods. It will be autowired in TutorialController, CommentController.
TutorialController, CommentController 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>com.mysql</groupId>
	<artifactId>mysql-connector-j</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.MySQLDialect

# 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 MySQLDialect for MySQL or PostgreSQLDialect for PostgreSQL
  • spring.jpa.hibernate.ddl-auto is used for database initialization. We set the value to update 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 be validate.

– 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 and jdbc: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 url http://localhost:8080/h2-console will change to http://localhost:8080/h2-ui.

Define Data Model for JPA One to Many Unidirectional mapping

In model package, we define Tutorial and Comment class.

Tutorial has five fields: id, title, description, published, comments. But in the database table, it will show only four (without comments).

model/Tutorial.java

package com.bezkoder.spring.hibernate.onetomany.model;

import java.util.HashSet;
import java.util.Set;

import jakarta.persistence.*;

@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;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
  @JoinColumn(name = "tutorial_id")
  private Set<Comment> comments = new HashSet<>();

  public Tutorial() {

  }

  public Tutorial(String title, String description, boolean published) {
    this.title = title;
    this.description = description;
    this.published = published;
  }

  // getters and setters
}

@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. GenerationType.IDENTITY indicates that the persistence provider must assign primary keys for the entity using a database identity column.

@Column annotation is used to define the column in database that maps annotated field.

@OneToMany annotation is for One-to-Many Unidirectional mapping. We use it together with @JoinColumn annotation that will create a column in child table storing the parent entity id (tutorial_id).

You also see that we indicate orphanRemoval = true. What is it?
It helps us remove child entity (comment record) from database if we remove it from the parent’s collection (comments field).

Now we only need to define the Comment class without caring about relationship.

model/Comment.java

package com.bezkoder.spring.hibernate.onetomany.model;

import jakarta.persistence.*;

@Entity
@Table(name = "comments")
public class Comment {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Lob
  private String content;

  // getters and setters
}

Create Repository Interfaces for One To Many Unidirectional mapping

Let’s create a repository to interact with database.
In repository package, create TutorialRepository and CommentRepository interfaces that extend JpaRepository.

repository/TutorialRepository.java

package com.bezkoder.spring.hibernate.onetomany.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.bezkoder.spring.hibernate.onetomany.model.Tutorial;

@Repository
public interface TutorialRepository extends JpaRepository<Tutorial, Long> {
  List<Tutorial> findByPublished(boolean published);

  List<Tutorial> findByTitleContaining(String title);
}

repository/CommentRepository.java

package com.bezkoder.spring.hibernate.onetomany.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.bezkoder.spring.hibernate.onetomany.model.Comment;

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {

}

Now we can use JpaRepository’s methods: save(), findOne(), findById(), findAll(), count(), delete(), deleteById()… without implementing these methods.

The implementation is plugged in by Spring Data JPA automatically.

You may also define custom finder methods if necessary:

  • findByPublished(): returns all Tutorials with published having value as input published.
  • findByTitleContaining(): returns all Tutorials which title contains input title.

More Derived queries at:
JPA Repository query example in Spring Boot

Create Spring Rest APIs Controller

Finally, we create controller that provides APIs for CRUD operations: creating, retrieving, updating, deleting and finding Tutorials and Comments.

controller/TutorialController.java

package com.bezkoder.spring.hibernate.onetomany.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.onetomany.exception.ResourceNotFoundException;
import com.bezkoder.spring.hibernate.onetomany.model.Tutorial;
import com.bezkoder.spring.hibernate.onetomany.repository.TutorialRepository;

@CrossOrigin(origins = "*")
@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/CommentController.java

package com.bezkoder.spring.hibernate.onetomany.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.onetomany.exception.ResourceNotFoundException;
import com.bezkoder.spring.hibernate.onetomany.model.Comment;
import com.bezkoder.spring.hibernate.onetomany.model.Tutorial;
import com.bezkoder.spring.hibernate.onetomany.repository.CommentRepository;
import com.bezkoder.spring.hibernate.onetomany.repository.TutorialRepository;

@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api")
public class CommentController {

  @Autowired
  private TutorialRepository tutorialRepository;

  @Autowired
  private CommentRepository commentRepository;

  @GetMapping("/tutorials/{tutorialId}/comments")
  public ResponseEntity<List<Comment>> getAllCommentsByTutorialId(@PathVariable(value = "tutorialId") Long tutorialId) {    
    Tutorial tutorial = tutorialRepository.findById(tutorialId)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));

    List<Comment> comments = new ArrayList<Comment>();
    comments.addAll(tutorial.getComments());
    
    return new ResponseEntity<>(comments, HttpStatus.OK);
  }

  @GetMapping("/comments/{id}")
  public ResponseEntity<Comment> getCommentsByTutorialId(@PathVariable(value = "id") Long id) {
    Comment comment = commentRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Comment with id = " + id));

    return new ResponseEntity<>(comment, HttpStatus.OK);
  }

  @PostMapping("/tutorials/{tutorialId}/comments")
  public ResponseEntity<Comment> createComment(@PathVariable(value = "tutorialId") Long tutorialId,
      @RequestBody Comment commentRequest) {
    Comment comment = tutorialRepository.findById(tutorialId).map(tutorial -> {
      tutorial.getComments().add(commentRequest);
      return commentRepository.save(commentRequest);
    }).orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));

    return new ResponseEntity<>(comment, HttpStatus.CREATED);
  }

  @PutMapping("/comments/{id}")
  public ResponseEntity<Comment> updateComment(@PathVariable("id") long id, @RequestBody Comment commentRequest) {
    Comment comment = commentRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("CommentId " + id + "not found"));

    comment.setContent(commentRequest.getContent());

    return new ResponseEntity<>(commentRepository.save(comment), HttpStatus.OK);
  }

  @DeleteMapping("/comments/{id}")
  public ResponseEntity<HttpStatus> deleteComment(@PathVariable("id") long id) {
    commentRepository.deleteById(id);

    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
  }
  
  @DeleteMapping("/tutorials/{tutorialId}/comments")
  public ResponseEntity<List<Comment>> deleteAllCommentsOfTutorial(@PathVariable(value = "tutorialId") Long tutorialId) {
    Tutorial tutorial = tutorialRepository.findById(tutorialId)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));
    
    tutorial.removeComments();
    tutorialRepository.save(tutorial);
    
    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
  }
}

Conclusion

Today we’ve built a JPA One To Many Unidirectional mapping in a Spring Boot example using Spring Data JPA, Hibernate with MySQL/PostgreSQL/embedded database (H2).

We also see that JpaRepository supports a great way to make CRUD operations, custom finder methods without need of boilerplate code.

With @OneToMany, we need to declare a collection inside parent class, we cannot limit the size of that collection, for example, in case of pagination.

@ManyToOne annotation is the most appropriate way for implementing JPA One to Many Mapping, and with it, you can modify Repository:

For @ManyToOne tutorial, please visit: JPA Many to One example

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

Handle Exception for this Rest APIs is necessary:
Spring Boot @ControllerAdvice & @ExceptionHandler example
@RestControllerAdvice example in Spring Boot

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
– or: Docker Compose: Spring Boot and Postgres 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

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
Angular 15 + Spring Boot example
Angular 16 + Spring Boot example
React + Spring Boot example

Source Code

You can find the complete source code for this tutorial on Github.

Many-to-One: Spring JPA Many to One example
Many-to-Many: Spring JPA Many to Many example
One-to-One: Spring JPA One to One example

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

Documentation: Spring Boot + Swagger 3 example (with OpenAPI 3)
Caching: Spring Boot Redis Cache example