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 Instructor can have multiple courses.

Video

This tutorial is explained in the below Youtube Video. 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 2+
  2. Hibernate 5+
  3. JDK 1.8+
  4. Maven 3+
  5. IDE - STS or Eclipse
  6. Spring Data JPA
  7. MySQL 5+

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:
<?xml version="1.0" encoding="UTF-8"?>
<project
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>net.guides.springboot2</groupId>
    <artifactId>springboot-jpa-one-to-many-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath />
        <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <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>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

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.MySQL57Dialect
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 javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

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 javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@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 javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@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

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 javax.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 javax.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

Post a Comment