Spring Boot JPA/Hibernate One to Many Example Tutorial

In this tutorial, we will learn how to implement step by step one-to-many bidirectional entity mapping using JPA/ Hibernate with Spring Boot, Spring Data JPA, and MySQL database.

In this example, we will implement a one-to-many relationship between the Instructor and Course entities. 
One to Many mapping example - One Instructor have multiple courses.

Video Tutorial - Spring Boot JPA/Hibernate One to Many Example Tutorial

Spring Boot JPA/Hibernate One to Many Video Tutorial. Subscribe to my youtube channel to learn more about Spring boot at Java Guides - YouTube Channel.

Overview

Simply put, one-to-many mapping means that one row in a table is mapped to multiple rows in another table.
Let’s look at the following entity-relationship diagram to see a one-to-many association.
One Instructor can have multiple courses:

Tools and Technologies used

  1. Spring Boot 3
  2. Hibernate 6 
  3. JDK 17 or later
  4. Maven 3+
  5. IDE - STS or Eclipse
  6. Spring Data JPA
  7. MySQL 8+

Development Steps

  1. Create Spring boot application
  2. Project dependencies
  3. Project Structure
  4. Configuring the Database and Logging
  5. Defining the Domain Models
  6. Defining the Repositories
  7. CRUD Restful web services for Instructor and Course Resources
  8. Enabling JPA Auditing
  9. Run the application

1. Create a Spring Boot Application

There are many ways to create a Spring Boot application. You can refer below articles to create a Spring Boot application.
Refer project structure or packaging structure from step 3.

2. Maven Dependencies

Let's add required maven dependencies to pom.xml:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

3. Project Structure

Let's refer below screenshot to create our Project packaging structure - 

4. Configuring the Database and Hibernate Log levels

We’ll need to configure MySQL database URLusername, and password so that Spring can establish a connection with the database on startup.
Open src/main/resources/application.properties and add the following properties to it -
logging.pattern.console=%clr(%d{yy-MM-dd E HH:mm:ss.SSS}){blue} %clr(%-5p) %clr(%logger{0}){blue} %clr(%m){faint}%n

spring.datasource.url=jdbc:mysql://localhost:3306/demo?useSSL=false
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true

logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE

5. Defining the Domain Models

We use Spring Boot’s JPA Auditing feature to automatically populate the created_at and updated_at fields while persisting the entities.

AuditModel

We’ll abstract out these common fields in a separate class called AuditModel and extend this class in the Instructor and Course entities.
package net.guides.springboot.jparepository.model;

import java.io.Serializable;
import java.util.Date;

import jakarta.persistence.*;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;


@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)

public abstract class AuditModel implements Serializable {

    private static final long serialVersionUID = 1 L;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", nullable = false, updatable = false)
    @CreatedDate
    private Date createdAt;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "updated_at", nullable = false)
    @LastModifiedDate
    private Date updatedAt;

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    public Date getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(Date updatedAt) {
        this.updatedAt = updatedAt;
    }
}

Instructor Domain Model - Instructor.java

package net.guides.springboot.jparepository.model;

import java.util.List;

import jakarta.persistence.*;

@Entity
@Table(name = "instructor")
public class Instructor extends AuditModel {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;

    @OneToMany(mappedBy = "instructor", cascade = {
        CascadeType.ALL
    })
    private List < Course > courses;

    public Instructor() {

    }

    public Instructor(String firstName, String lastName, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public List < Course > getCourses() {
        return courses;
    }

    public void setCourses(List < Course > courses) {
        this.courses = courses;
    }
}

Course Domain Model - Course.java

package net.guides.springboot.jparepository.model;

import jakarta.persistence.*;

@Entity
@Table(name = "course")
public class Course extends AuditModel {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "title")
    private String title;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "instructor_id")
    private Instructor instructor;

    public Course() {

    }

    public Course(String title) {
        this.title = title;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Instructor getInstructor() {
        return instructor;
    }

    public void setInstructor(Instructor instructor) {
        this.instructor = instructor;
    }

    @Override
    public String toString() {
        return "Course [id=" + id + ", title=" + title + "]";
    }
}
  • @Table maps the entity with the table. If no @Table is defined, the default value is used: the class name of the entity.
  • @Id declares the identifier property of the entity.
  • @Column maps the entity's field with the table's column. If @Column is omitted, the default value is used: the field name of the entity.
  • @OneToMany and @ManyToOne defines a one-to-many and many-to-one relationship between 2 entities. @JoinColumn indicates the entity is the owner of the relationship: the corresponding table has a column with a foreign key to the referenced table. mappedBy indicates the entity is the inverse of the relationship.

6. Enabling JPA Auditing

To enable JPA Auditing, you’ll need to add @EnableJpaAuditing annotation to one of your configuration classes. Open the main class Application.java and add the @EnableJpaAuditing to the main class like so -
package net.guides.springboot.jparepository;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing //  Enabling JPA Auditing
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

7. Defining the Repositories

Spring Data JPA contains some built-in Repository implemented some common functions to work with database: findOne, findAll, save,...All we need for this example is to extend it.

InstructorRepository

package net.guides.springboot.jparepository.repository;

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

import net.guides.springboot.jparepository.model.Instructor;

@Repository
public interface InstructorRepository extends JpaRepository<Instructor, Long>{

}

CourseRepository

package net.guides.springboot.jparepository.repository;

import java.util.List;
import java.util.Optional;

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

import net.guides.springboot.jparepository.model.Course;

@Repository
public interface CourseRepository extends JpaRepository<Course, Long>{
 List<Course> findByInstructorId(Long instructorId);
 Optional<Course> findByIdAndInstructorId(Long id, Long instructorId);
}

8. CRUD Restful Web Services Instructor and Course Resources

ResourceNotFoundException

Lets first create a ResourceNotFoundException.java class. This custom exception we use in the Spring Rest controller to throw ResourceNotFoundException if record not found in the database.
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends Exception{

    private static final long serialVersionUID = 1L;

    public ResourceNotFoundException(String message){
        super(message);
    }
}

REST APIs for Instructor Resource - InstructorController

package net.guides.springboot.jparepository.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jakarta.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import net.guides.springboot.jparepository.model.Instructor;
import net.guides.springboot.jparepository.repository.InstructorRepository;

@RestController
@RequestMapping("/api/v1")
public class InstructorController {

    @Autowired
    private InstructorRepository instructorRepository;


    @GetMapping("/instructors")
    public List < Instructor > getInstructors() {
        return instructorRepository.findAll();
    }

    @GetMapping("/instructors/{id}")
    public ResponseEntity < Instructor > getInstructorById(
        @PathVariable(value = "id") Long instructorId) throws ResourceNotFoundException {
        Instructor user = instructorRepository.findById(instructorId)
            .orElseThrow(() -> new ResourceNotFoundException("Instructor not found :: " + instructorId));
        return ResponseEntity.ok().body(user);
    }

    @PostMapping("/instructors")
    public Instructor createUser(@Valid @RequestBody Instructor instructor) {
        return instructorRepository.save(instructor);
    }

    @PutMapping("/instructors/{id}")
    public ResponseEntity < Instructor > updateUser(
        @PathVariable(value = "id") Long instructorId,
        @Valid @RequestBody Instructor userDetails) throws ResourceNotFoundException {
        Instructor user = instructorRepository.findById(instructorId)
            .orElseThrow(() -> new ResourceNotFoundException("Instructor not found :: " + instructorId));
        user.setFirstName(userDetails.getFirstName());
        user.setLastName(userDetails.getLastName());
        user.setEmail(userDetails.getEmail());
        final Instructor updatedUser = instructorRepository.save(user);
        return ResponseEntity.ok(updatedUser);
    }

    @DeleteMapping("/instructors/{id}")
    public Map < String, Boolean > deleteUser(
        @PathVariable(value = "id") Long instructorId) throws ResourceNotFoundException {
        Instructor instructor = instructorRepository.findById(instructorId)
            .orElseThrow(() -> new ResourceNotFoundException("Instructor not found :: " + instructorId));

        instructorRepository.delete(instructor);
        Map < String, Boolean > response = new HashMap < > ();
        response.put("deleted", Boolean.TRUE);
        return response;
    }
}

REST APIs for Course - CourseController

package net.guides.springboot.jparepository.controller;

import java.util.List;

import jakarta.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import net.guides.springboot.jparepository.model.Course;
import net.guides.springboot.jparepository.repository.CourseRepository;
import net.guides.springboot.jparepository.repository.InstructorRepository;

@RestController
public class CourseController {

    @Autowired
    private CourseRepository courseRepository;

    @Autowired
    private InstructorRepository instructorRepository;

    @GetMapping("/instructors/{instructorId}/courses")
    public List < Course > getCoursesByInstructor(@PathVariable(value = "postId") Long instructorId) {
        return courseRepository.findByInstructorId(instructorId);
    }

    @PostMapping("/instructors/{instructorId}/courses")
    public Course createCourse(@PathVariable(value = "instructorId") Long instructorId,
        @Valid @RequestBody Course course) throws ResourceNotFoundException {
        return instructorRepository.findById(instructorId).map(instructor - > {
            course.setInstructor(instructor);
            return courseRepository.save(course);
        }).orElseThrow(() -> new ResourceNotFoundException("instructor not found"));
    }

    @PutMapping("/instructors/{instructorId}/courses/{courseId}")
    public Course updateCourse(@PathVariable(value = "instructorId") Long instructorId,
        @PathVariable(value = "courseId") Long courseId, @Valid @RequestBody Course courseRequest)
    throws ResourceNotFoundException {
        if (!instructorRepository.existsById(instructorId)) {
            throw new ResourceNotFoundException("instructorId not found");
        }

        return courseRepository.findById(courseId).map(course - > {
            course.setTitle(courseRequest.getTitle());
            return courseRepository.save(course);
        }).orElseThrow(() -> new ResourceNotFoundException("course id not found"));
    }

    @DeleteMapping("/instructors/{instructorId}/courses/{courseId}")
    public ResponseEntity < ? > deleteCourse(@PathVariable(value = "instructorId") Long instructorId,
        @PathVariable(value = "courseId") Long courseId) throws ResourceNotFoundException {
        return courseRepository.findByIdAndInstructorId(courseId, instructorId).map(course - > {
            courseRepository.delete(course);
            return ResponseEntity.ok().build();
        }).orElseThrow(() -> new ResourceNotFoundException(
            "Course not found with id " + courseId + " and instructorId " + instructorId));
    }
}

9. Run the application

package net.guides.springboot.jparepository;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing // Enabling JPA Auditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

9. Output


Comments

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi excuse me, uhm ResourceNotFoundException code?

    ReplyDelete
    Replies
    1. Added to this tutorial. Thanks for reporting !.

      Delete
  3. Hi great tutorial!

    Could you please add Postman request sample for each endpoint here?

    ReplyDelete
  4. Hi, Can you explain me the post_id value in get mapping course controller.. I am getting an error while fetching list of tickets in post_id value.

    ReplyDelete
  5. Great tutorial, can you explain why InstructorRepository is blank ?

    ReplyDelete

  6. Hi great tutorial! I would like to post several courses at the same time on REST APIs for Course - CourseController ... ie List !! please help me

    ReplyDelete
  7. Thank you for the very nice tutorial.
    But when tested, Instructor ID foreign key is not stored in the course table.

    ID CREATED_AT UPDATED_AT TITLE INSTRUCTOR_ID
    1 2020-07-25 15:00:58.207 2020-07-25 15:00:58.207 Java Language null
    2 2020-07-25 15:00:58.211 2020-07-25 15:00:58.211 Spring Boot null

    ReplyDelete
  8. Sir, can you extend this eg to a webmvc implementation. Thanks for tutorial. Very well explained.

    ReplyDelete
  9. I was having a 1toMany problem in my springboot project, so this tutorial looked perfect for me.
    Problem is I'm having the same problem with this I had with my project.
    I inserted two entries into the 'instructor' table
    "id": 1,
    "firstName": "Bill",
    "lastName": "Billson",
    "email": "abc@def.com",
    "id": 2,
    "firstName": "Sam",
    "lastName": "Samuels",
    "email": "xyz@abc.com",
    And one entry on the 'course' table
    "id": 1,
    "title": "Small Adventures",
    "instructor_id": 1,

    when I do the basic GET
    http://localhost:8080:/api/v1/instructors
    I get:
    "createdAt": "2020-08-01T15:08:01.000+00:00",
    "updatedAt": "2020-08-01T15:08:01.000+00:00",
    "id": 1,
    "firstName": "Bill",
    "lastName": "Billson",
    "email": "abc@def.com",
    "courses": [
    {
    "createdAt": "2020-08-01T15:22:36.000+00:00",
    "updatedAt": "2020-08-01T15:22:36.000+00:00",
    "id": 1,
    "title": "Small Adventures",
    "instructor": {
    "createdAt": "2020-08-01T15:08:01.000+00:00",
    "updatedAt": "2020-08-01T15:08:01.000+00:00",
    "id": 1,
    "firstName": "Bill",
    "lastName": "Billson",
    "email": "abc@def.com",
    "courses": [
    {
    "createdAt": "2020-08-01T15:22:36.000+00:00",
    "updatedAt": "2020-08-01T15:22:36.000+00:00",
    "id": 1,
    "title": "Small Adventures",
    "instructor": {
    "createdAt": "2020-08-01T15:08:01.000+00:00",
    "updatedAt": "2020-08-01T15:08:01.000+00:00",
    "id": 1,
    "firstName": "Bill",
    "lastName": "Billson",
    "email": "abc@def.com",
    "courses": [
    {
    "createdAt": "2020-08-01T15:22:36.000+00:00",
    "updatedAt": "2020-08-01T15:22:36.000+00:00",
    "id": 1,
    "title": "Small Adventures",
    "instructor": {
    "createdAt": "2020-08-01T15:08:01.000+00:00",
    "updatedAt": "2020-08-01T15:08:01.000+00:00",
    "id": 1,
    "firstName": "Bill",
    .....
    .....
    You can see my problem, this loops and repeats over and over rather than just getting a list of courses for the instructors

    So, what gives? Why the infinite feedback loop?

    ReplyDelete
    Replies
    1. Not sure if your issue is resolved yet but you can try the following.

      instructor POJO fix it like below:
      @OneToMany(mappedBy = "instructor", fetch = FetchType.LAZY, cascade = {CascadeType.ALL})
      private List < Course > courses;

      and for courses POJO fix like below:

      @ManyToOne(cascade = CascadeType.ALL)
      @JoinColumn(name = "instructor_id")
      @JsonBackReference
      private Instructor instructor;

      Delete
    2. Please remove the getter of instructor from course entity.

      Delete
    3. removing the getter of instructor from course entity works , can you explain why?

      Delete
  10. If i want to show data on UI using JOIN of these two table "Post" and "Comment" how i can proceed for that . can you please help me out on this .

    I have created REST service for SAVING DATA , now want one Web Service where all data will get populated .

    ReplyDelete
  11. Please, I would like to add many course at the same time ... how do we do it? help me ... thank you

    I do like this but there is an error:

    @PostMapping("/instructors/{instructorId}/courses")
    public List createCourse(@PathVariable(value = "instructorId") Long instructorId,
    @Valid @RequestBody List course) throws ResourceNotFoundException {
    return instructorRepository.findById(instructorId).map(instructor - > {
    course.setInstructor(instructor); // error here
    return courseRepository.saveAll(course);
    }).orElseThrow(() - > new ResourceNotFoundException("instructor not found"));
    }

    ReplyDelete
  12. try change saveAll to save

    ReplyDelete
  13. I get error when running : plz let me know how to fix

    Error creating bean with name 'requestMappingHandlerAdapter' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Unsatisfied dependency expressed through method 'requestMappingHandlerAdapter' parameter 2; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mvcValidator' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.validation.Validator]: Factory method 'mvcValidator' threw exception; nested exception is java.lang.NoClassDefFoundError: javax/validation/ParameterNameProvider

    ReplyDelete
  14. where you created this orElseThrow()? can you please help me with this.

    ReplyDelete

Post a Comment

Leave Comment