A Guide to JPA with Hibernate (Relationship Mappings) — OneToOne , OneToMany , ManyToOne , ManyToMany

Introduction

In this article, we’ll dive into Relationship Mapping with JPA and Hibernate in Java.

What we will cover here:

Many-to-One, One-to-Many, Many-to-Many and One-to-One relationships. Additionally, we’ll also cover cascading operations, bidirectionality, optionality and eager/lazy loading fetch-types

Our Example

Before getting started, let’s remind us of the example we used in the previous part of this series. The idea was to map the model of a school with students taking courses given by teachers.

Relationships

First of all, let’s define a relationship. If we look at our class diagram we can see a few relationships:

  • Many-to-One
  • One-to-One
  • Many-to-Many

One-to-Many/Many-to-One

We’ll get started with the One-to-Many and Many-to-One relationships, which are closely related. You could go ahead and say that they’re the opposite sides of the same coin.

@Entity
public class Teacher {
private String firstName;
private String lastName;
}
@Entity
public class Course {
private String title;
}
@OneToMany
private List<Course> courses;
@OneToMany
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private List<Course> courses;
insert into TEACHER(ID, LASTNAME, FIRSTNAME) values(1, 'Doe', 'Jane');insert into COURSE(ID, TEACHER_ID, TITLE) values(1, 1, 'Java 101');
insert into COURSE(ID, TEACHER_ID, TITLE) values(2, 1, 'SQL 101');
insert into COURSE(ID, TEACHER_ID, TITLE) values(3, 1, 'JPA 101');
Teacher foundTeacher = entityManager.find(Teacher.class, 1L);assertThat(foundTeacher.id()).isEqualTo(1L);
assertThat(foundTeacher.lastName()).isEqualTo("Doe");
assertThat(foundTeacher.firstName()).isEqualTo("Jane");
assertThat(foundTeacher.courses())
.extracting(Course::title)
.containsExactly("Java 101", "SQL 101", "JPA 101");

Owning Side and Bidirectionality

In the previous example, the Teacher class is called the owning side of the One-To-Many relationship. This is because it defines the join column between the two tables.

@ManyToOne
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;
Course foundCourse = entityManager.find(Course.class, 1L);assertThat(foundCourse.id()).isEqualTo(1L);
assertThat(foundCourse.title()).isEqualTo("Java 101");
assertThat(foundCourse.teacher().lastName()).isEqualTo("Doe");
assertThat(foundCourse.teacher().firstName()).isEqualTo("Jane");
@Entity
public class Teacher {
// ...
@OneToMany(mappedBy = "teacher")
private List<Course> courses;
}
@Entity
public class Course {
// ...

@ManyToOne
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;
}

Eager vs Lazy Loading

Another thing worth noting is eager and lazy loading. With all our relationships mapped, it’s wise to avoid impacting the software’s memory by putting too many entities in it if unnecessary.

@OneToMany(mappedBy = "teacher", fetch = FetchType.EAGER)
private List<Course> courses;
@ManyToOne(fetch = FetchType.LAZY)
private Teacher teacher;

Optionality

Now, let’s talk about optionality.

Course course = new Course("C# 101");
entityManager.persist(course);
@ManyToOne(optional = false)
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;
Course course = new Course("C# 101");
assertThrows(Exception.class, () -> entityManager.persist(course));
Teacher teacher = new Teacher();
teacher.setLastName("Doe");
teacher.setFirstName("Will");
Course course = new Course("C# 101");
course.setTeacher(teacher);
entityManager.persist(course);
javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.fdpro.clients.stackabuse.jpa.domain.Course
Teacher teacher = new Teacher();
teacher.setLastName("Doe");
teacher.setFirstName("Will");
entityManager.persist(teacher);
Course course = new Course("C# 101");
course.setTeacher(teacher);
entityManager.persist(course);
entityManager.flush();

Cascading Operations

However, we could’ve done another thing — we could’ve cascaded, and thus propagated the persistence of the Teacher object when we persist the Course object.

@ManyToOne(optional = false, cascade = CascadeType.PERSIST)
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;

One-to-One

Now that we’ve set up the foundations of relationship mapping in JPA through One-to-Many/Many-to-One relationships and their settings, we can move on to One-to-One relationships.

@Entity
public class CourseMaterial {
@Id
private Long id;
private String url;
}
@OneToOne(optional = false)
@JoinColumn(name = "COURSE_ID", referencedColumnName = "ID")
private Course course;
@OneToOne(mappedBy = "course")
private CourseMaterial material;

Many-to-Many

Now, last but not least: Many-to-Many relationships. We kept these for the end because they require a bit more work than the previous ones.

@ManyToMany
@JoinTable(
name = "STUDENTS_COURSES",
joinColumns = @JoinColumn(name = "COURSE_ID", referencedColumnName = "ID"),
inverseJoinColumns = @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID")
)
private List<Student> students;
Student johnDoe = new Student();
johnDoe.setFirstName("John");
johnDoe.setLastName("Doe");
johnDoe.setBirthDateAsLocalDate(LocalDate.of(2000, FEBRUARY, 18));
johnDoe.setGender(MALE);
johnDoe.setWantsNewsletter(true);
johnDoe.setAddress(new Address("Baker Street", "221B", "London"));
entityManager.persist(johnDoe);
Student willDoe = new Student();
willDoe.setFirstName("Will");
willDoe.setLastName("Doe");
willDoe.setBirthDateAsLocalDate(LocalDate.of(2001, APRIL, 4));
willDoe.setGender(MALE);
willDoe.setWantsNewsletter(false);
willDoe.setAddress(new Address("Washington Avenue", "23", "Oxford"));
entityManager.persist(willDoe);
Teacher teacher = new Teacher();
teacher.setFirstName("Jane");
teacher.setLastName("Doe");
entityManager.persist(teacher);
Course javaCourse = new Course("Java 101");
javaCourse.setTeacher(teacher);
entityManager.persist(javaCourse);
Course sqlCourse = new Course("SQL 101");
sqlCourse.setTeacher(teacher);
entityManager.persist(sqlCourse);
public class Course {    private List<Student> students = new ArrayList<>();    public void addStudent(Student student) {
this.students.add(student);
}
}
Course javaCourse = new Course("Java 101");
javaCourse.setTeacher(teacher);
javaCourse.addStudent(johnDoe);
javaCourse.addStudent(willDoe);
entityManager.persist(javaCourse);
Course sqlCourse = new Course("SQL 101");
sqlCourse.setTeacher(teacher);
sqlCourse.addStudent(johnDoe);
entityManager.persist(sqlCourse);
Course courseWithMultipleStudents = entityManager.find(Course.class, 1L);assertThat(courseWithMultipleStudents).isNotNull();
assertThat(courseWithMultipleStudents.students())
.hasSize(2)
.extracting(Student::firstName)
.containsExactly("John", "Will");

Conclusion

That concludes this article about relationships of mapped entities with JPA. We’ve covered Many-to-One, One-to-Many, Many-to-Many and One-to-One relationships. Additionally, we’ve explored cascading operations, bidirectionality, optionality and eager/lazy loading fetch-types.

Software Developer