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);
}
}
This comment has been removed by the author.
ReplyDeleteHi excuse me, uhm ResourceNotFoundException code?
ReplyDeleteAdded to this tutorial. Thanks for reporting !.
DeleteHi great tutorial!
ReplyDeleteCould you please add Postman request sample for each endpoint here?
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.
ReplyDeleteGreat tutorial, can you explain why InstructorRepository is blank ?
ReplyDeleteto apply CRUD operations
Delete
ReplyDeleteHi great tutorial! I would like to post several courses at the same time on REST APIs for Course - CourseController ... ie List !! please help me
Thank you for the very nice tutorial.
ReplyDeleteBut 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
Sir, can you extend this eg to a webmvc implementation. Thanks for tutorial. Very well explained.
ReplyDeleteI was having a 1toMany problem in my springboot project, so this tutorial looked perfect for me.
ReplyDeleteProblem 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?
Not sure if your issue is resolved yet but you can try the following.
Deleteinstructor 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;
Please remove the getter of instructor from course entity.
Deleteremoving the getter of instructor from course entity works , can you explain why?
DeleteIf 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 .
ReplyDeleteI have created REST service for SAVING DATA , now want one Web Service where all data will get populated .
Please, I would like to add many course at the same time ... how do we do it? help me ... thank you
ReplyDeleteI 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"));
}
try change saveAll to save
ReplyDeleteI get error when running : plz let me know how to fix
ReplyDeleteError 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
where you created this orElseThrow()? can you please help me with this.
ReplyDelete