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.

JPA is the persistence standard of the Java ecosystem. It allows us to map our domain model directly to the database structure and then gives us the flexibility of manipulating objects in our code — instead of messing with cumbersome JDBC components like , , etc.

We’ll be making a comprehensive guide to using JPA with Hibernate as its vendor. In this article, we’ll be covering relationship mappings. It will going to be a bit longer article but you will get overall clarity, so be focused.

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.

Here is what this model looks like:

As we can see, there are a few classes with certain properties. These classes have relationships between them. By the end of this article, we’ll have mapped all those classes to database tables, perserving their relationships.

Furthermore, we’ll be able to retrieve them and manipulate them as objects, without the hassle of JDBC.

Relationships

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

Teachers and courses — students and courses — courses and course materials.

There are also connections between students and addresses, but they aren’t considered relationships. This is because an is not an entity (i.e. it is not mapped to a table of its own). So, as far as JPA's concerned, it's not a relationship.

There are a few types of relationships:

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

Let’s tackle these relationships one by one.

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.

What’s a One-to-Many relationship?

As its name implies, it’s a relationship that links one entity to many other entities.

In our example, this would be a and their . A teacher can give multiple courses, but a course is given by only one teacher (that's the Many-to-One perspective - many courses to one teacher).

Another example could be on social media — a photo can have many comments, but each of those comments belongs to that one photo.

Before diving into the details of how to map this relationship, let’s create our entities:

@Entity
public class Teacher {
private String firstName;
private String lastName;
}
@Entity
public class Course {
private String title;
}

Now, the fields of the class should include a list of courses. Since we'd like to map this relationship in a database, which can't include a list of entities within another entity - we'll annotate it with a annotation:

@OneToMany
private List<Course> courses;

We’ve used a as the field type here, but we could've gone for a or a (though this one requires a bit more configuration).

How does JPA reflect this relationship in the database? Generally, for this type of relationship, we must use a foreign key in a table.

JPA does this for us, given our input on how it should handle the relationship. This is done via the annotation:

@OneToMany
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private List<Course> courses;

Using this annotation will tell JPA that the table must have a foreign key column that references the table's column.

Let’s add some data to those tables:

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');

And now let’s check if the relationship works as expected:

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");

We can see that the teacher’s courses are gathered automatically, when we retrieve the instance.

Owning Side and Bidirectionality

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

The is called the referencing side in that relationship.

We could’ve made the owning side of the relationship by mapping the field with in the class instead:

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

There’s no need to have a list of courses in the class now. The relationship would've worked the opposite way:

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");

This time, we used the annotation, in the same way we used .

Note: It’s a good practice to put the owning side of a relationship in the class/table where the foreign key will be held.

So, in our case this second version of the code is better. But, what if we still want our class to offer access to its list?

We can do that by defining a bidirectional relationship:

@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;
}

We keep our mapping on the entity. However, we also map a list of s to the entity.

What’s important to note here is the use of the flag in the annotation on the referencing side.

Without it, we wouldn’t have a two-way relationship. We’d have two one-way relationships. Both entities would be mapping foreign keys for the other entity.

With it, we’re telling JPA that the field is already mapped by another entity. It’s mapped by the field of the entity.

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.

Imagine that is a heavy object, and we load all objects from the database for some operation. We don't need to retrieve or use the courses for this operation, but they're still being loaded alongside the objects.

This can be devastating for the application’s performance. Technically, this can be solved by using the Data Transfer Object Design Pattern and retrieving information without the courses.

However, this can be a massive overkill if all we’re gaining from the pattern is excluding the courses.

Thankfully, JPA thought ahead and made One-to-Many relationships load lazily by default.

This means that the relationship won’t be loaded right away, but only when and if actually needed.

In our example, that would mean until we call on the method, the courses are not being fetched from the database.

By contrast, Many-to-One relationships are eager by default, meaning the relationship is loaded at the same time the entity is.

We can change these characteristics by setting the argument of both annotations:

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

That would inverse the way it worked initially. Courses would be loaded eagerly, as soon as we load a object. By contrast, the wouldn't be loaded when we fetch if it's unneeded at the time.

Optionality

Now, let’s talk about optionality.

A relationship may be optional or mandatory.

Considering the One-to-Many side — it is always optional, and we can’t do anything about it. The Many-to-One side, on the other hand, offers us the option of making it mandatory.

By default, the relationship is optional, meaning we can save a without assigning it a teacher:

Course course = new Course("C# 101");
entityManager.persist(course);

Now, let’s make this relationship mandatory. To do that, we’ll use the argument of the annotation and set it to (it's by default):

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

Thus, we can no longer save a course without assigning a teacher to it:

Course course = new Course("C# 101");
assertThrows(Exception.class, () -> entityManager.persist(course));

But if we give it a teacher, it works fine again:

Teacher teacher = new Teacher();
teacher.setLastName("Doe");
teacher.setFirstName("Will");
Course course = new Course("C# 101");
course.setTeacher(teacher);
entityManager.persist(course);

Well, at least, it would seem so. If we had run the code, an exception would’ve been thrown:

javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.fdpro.clients.stackabuse.jpa.domain.Course

Why is this? We’ve set a valid object in the object we're trying to persist. However, we haven't persisted the object before trying to persist the object.

Thus, the object isn't a managed entity. Let's fix that and try again:

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();

Running this code will persist both entities and perserve the relationship between them.

Cascading Operations

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

This makes more sense and works the way we’d expect it to like in the first example which threw an exception.

To do this, we’ll modify the flag of the annotation:

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

This way, Hibernate knows to persist the needed object in this relationship as well.

There are multiple types of cascading operations: , , , , , and (that combines all the previous ones).

We can also put the cascade argument on the One-to-Many side of the relationship, so that operations are cascaded from teachers to their courses as well.

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.

This time, instead of having a relationship between one entity on one side and a bunch of entities on the other, we’ll have a maximum of one entity on each side.

This is, for example, the relationship between a and its . Let's first map , which we haven't done yet:

@Entity
public class CourseMaterial {
@Id
private Long id;
private String url;
}

The annotation for mapping a single entity to a single other entity is, unshockingly, .

Before setting it up in our model, let’s remember that a relationship has an owning side — preferably the side which will hold the foreign key in the database.

In our example, that would be as it makes sense that it references a (though we could go the other way around):

@OneToOne(optional = false)
@JoinColumn(name = "COURSE_ID", referencedColumnName = "ID")
private Course course;

There is no point in having material without a course to encompass it. That’s why the relationship is not in that direction.

Speaking of direction, let’s make the relationship bidirectional, so we can access the material of a course if it has one. In the class, let's add:

@OneToOne(mappedBy = "course")
private CourseMaterial material;

Here, we’re telling Hibernate that the material within a is already mapped by the field of the entity.

Also, there’s no attribute here as it's by default, and we could imagine a course without material (from a very lazy teacher).

In addition to making the relationship bidirectional, we could also add cascading operations or make entities load eagerly or lazily.

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.

Effectively, in a database, a Many-to-Many relationship involves a middle table referencing both other tables.

Luckily for us, JPA does most of the work, we just have to throw a few annotations out there, and it handles the rest for us.

So, for our example, the Many-to-Many relationship will be the one between and instances as a student can attend multiple courses, and a course can be followed by multiple students.

In order to map a Many-to-Many relationship we’ll use the annotation. However, this time around, we'll also be using a annotation to set up the table that represents the relationship:

@ManyToMany
@JoinTable(
name = "STUDENTS_COURSES",
joinColumns = @JoinColumn(name = "COURSE_ID", referencedColumnName = "ID"),
inverseJoinColumns = @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID")
)
private List<Student> students;

Now, go over what’s going on here. The annotation takes a few parameters. First of all, we must give the table a name. We’ve chosen it to be .

After that, we’ll need to tell Hibernate which columns to join in order to populate . The first parameter, defines how to configure the join column (foreign key) of the owning side of the relationship in the table. In this case, the owning side is a .

On the other hand, the parameter does the same, but for the referencing side ().

Let’s set up a data set with students and courses:

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);

Of course, this won’t work out of the box. We’ll have to add a method that allows us to add students to a course. Let’s modify the class a bit:

public class Course {    private List<Student> students = new ArrayList<>();    public void addStudent(Student student) {
this.students.add(student);
}
}

Now, we can complete our dataset:

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);

Once this code has ran, it’ll persist our , and instances as well as their relationships. For example, let's retrieve a student from a persisted course and check if everything's fine:

Course courseWithMultipleStudents = entityManager.find(Course.class, 1L);assertThat(courseWithMultipleStudents).isNotNull();
assertThat(courseWithMultipleStudents.students())
.hasSize(2)
.extracting(Student::firstName)
.containsExactly("John", "Will");

Of course, we can still map the relationship as bidirectional the same way we did for the previous relationships.

We can also cascade operations as well as define if entities should load lazily or eagerly (Many-to-Many relationships are lazy by default).

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