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.
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
- Spring Boot 3
- Hibernate 6
- JDK 17 or later
- Maven 3+
- IDE - STS or Eclipse
- Spring Data JPA
- MySQL 8+
Development Steps
- Create Spring boot application
- Project dependencies
- Project Structure
- Configuring the Database and Logging
- Defining the Domain Models
- Defining the Repositories
- CRUD Restful web services for Instructor and Course Resources
- Enabling JPA Auditing
- 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.
>> Create Spring Boot Project With Spring Initializer
>> Create Spring Boot Project in Spring Tool Suite [STS]
>> Create Spring Boot Project in Spring Tool Suite [STS]
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 URL, username, 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
Free Spring Boot Tutorial | Full In-depth Course | Learn Spring Boot in 10 Hours
Watch this course on YouTube at Spring Boot Tutorial | Fee 10 Hours Full Course