Object Relational Mapping with Spring Boot, JPA and Hibernate

Let’s deal with object relationships

Since you know about the Spring Boot framework, you may have used JPA and Hibernate when working with relational databases like MySQL or PostgreSQL. Hibernate ORM is an object–relational mapping tool for the Java programming language. It provides a framework for mapping an object-oriented domain model to a relational database. In simpler terms, we can map Java objects to database tables! Hibernate is the default implementation of the JPA specification in Spring Boot.

This framework magically enhances the codebase with more reusability and maintainability. We can link just Java objects using few annotations straightforward. According to the rules we specify, then data will be saved in the database tables.

I will be using a Spring Boot application with H2 in-memory database to demonstrate the relationships practically. You can find the source code from here: https://github.com/SalithaUCSC/spring-boot-orm

All the dependencies used are available here: https://github.com/SalithaUCSC/spring-boot-orm/blob/main/pom.xml

Let’s start with some basics… 😎

Fetch Types in Hibernate

EAGER: Load the associated data of the other entity beforehand, which is a bit costly.

LAZY: Load the associated data of the other entity only when requested. This is done on demand.

There are specified fetching types for each relationship type which Hibernate applies by default:


OneToMany: LAZY
ManyToOne: EAGER
ManyToMany: LAZY
OneToOne: EAGER
    

Example:

If we have a relationship between university and student, when university data is fetched, we don’t want to fetch students right? Because, one university will have thousands of students in the students array in the mapping. It will be a very costly operation. So, we can tell Hibernate to keep it with LAZY initialization.

We can decide how to do that later…

Cascade Types in Hibernate

In Hibernate, Cascade Types define how operations should be propagated between related entities. There are several predefined types:

  1. CascadeType.PERSIST: Both save() or persist() operations cascade to related entities.
  2. CascadeType.MERGE: Related entities are merged when the ownership entity is merged.
  3. CascadeType.REFRESH: Does the same thing for the refresh() operation.
  4. CascadeType.REMOVE: Removes all related entities associated with this setting when the ownership entity is deleted.
  5. CascadeType.DETACH: Detaches all related entities if a “manual detach” occurs.
  6. CascadeType.ALL: All of the above cascade operations.

🔴 Important: There is no default cascade type in JPA. By default, no operation is cascaded. If we want, we can use several cascade types at once:


cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH }
    

Let’s start to learn about object-relational mapping with examples. 💪

One-to-One Mapping 💥

I’m going to build up a relationship between User and Address. Any User has an address here. Two users cannot have the same Address!

I don’t want the Address record without the relevant User. I think it can be the most common scenario. In a one-to-one mapping, both entities are tightly coupled. After the User is removed, we cannot use his/her Address. So I will define CascadeType as ALL (If you want to keep the Address, change it to PERSIST). Then address won’t be deleted even if we delete the user. Since Hibernate decides FetchType for one-to-one mapping is EAGER by default, I don’t want to mention it as a rule.

How relationship is built???

Normally we record the child entity primary key as the foreign key of the owner entity. So User should have a column in the table to record the address ID. I have given its name as “address_id” and it’s referenced by “id” column in Address entity.

User


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String name;
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_id", referencedColumnName = "id")
    Address address;
}
    

Address


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "addresses")
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String street;
    String city;
    @JsonIgnore
    @OneToOne(mappedBy = "address")
    User user;
}
    

In the child entity (Address), we just need to link the name of the property mapped in User entity.

“@JsonIgnore” annotation was placed there for user property since I do not need to have the user object to be seen in Address data. Just to ignore that field from JSON object.

This way we can have a Bi-directional one-to-one mapping! 💪

One-to-Many Mapping 💥

I’m creating another relationship between Post and Comment entities. Any kind of post can have one or more comments. But every comment is having only one post! So, this is a one-to-many relationship exactly…

I need to see the comments of each post while post data is fetched. That means, comments should be eagerly fetched right? That’s why I have put fetch type as EAGER for comments set. Hibernate will set fetch type as LAZY by default for one-to-many mappings. To override that, I have specifically mentioned the fetch type.

How relationship is built???

Tables should be connected in a way when the Comment table is having a column to place the primary key of the Post. That’s how we design the logical schema right… I have named the column “post_id” in this case. You may remember ER diagrams for this as I feel now.

Post


@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "posts")
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String title;
    String description;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinColumn(name = "post_id", referencedColumnName = "id")
    Set comments = new HashSet<>();
}
    

When a post is deleted, comments should not be there... It’s obvious. So, we can put CascadeType as ALL.

Comment


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "comments")
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String author;
    String content;
    @Column(name = "post_id")
    Long postId;
}
    

Comment has the owning entity primary key as described before.

This way we can have a uni-directional one-to-many mapping! Actually, in one-to-many scenarios, it is often sufficient to have a uni-directional relationship. No need to be bi-directional. 💪 We just need to make sure that the primary key of the Post is inserted into the Comment object while a comment is saved.

Many-to-Many Mapping 💥

Do you have any idea how we represented a many-to-many relationship in our logical design (ER diagram)?? This is a very special scenario, where we need an additional table more than the two entities.

Let me take an example and explain. In an Employee management system, every employee has one or more Role. Any employee can have one or more roles and any role can have one or more employees! Then that is many-to-many right?

How relationship is built???

Here, we have to record which employee has which role… That’s the need for the 3rd table! Simply we can save “employee_id” and “role_id” to keep the employee and role associations. If there are any other related things specific to this association, we can reserve more columns in the same table.

Employee


@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "employees")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String name;
    String email;
    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(
        name = "employee_roles",
        joinColumns = @JoinColumn(
            name = "employee_id", referencedColumnName = "id"
        ),
        inverseJoinColumns = @JoinColumn(
            name = "role_id", referencedColumnName = "id"
        )
    )
    Set roles = new HashSet<>();
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return id.equals(employee.id) && name.equals(employee.name) && email.equals(employee.email);
    }
    @Override
    public int hashCode() {
        return Objects.hash(id, name, email);
    }
}
    

Employee is the owning entity in the relationship. So, it’s annotated with “@ManyToMany”. We have to place join table config here. So, “@JoinTable” annotation represents this new table with name as “employee_roles”. There we have to give the two columns we use for the associations. They are primary keys in employees and roles tables.

I need to show the user roles when user data is retrieved. Since Hibernate considers LAZY as the default fetch type for ManyToMany mappings, I had to set up EAGER fetch there.

Role


@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String name;
    @ManyToMany(mappedBy = "roles")
    @JsonIgnore
    Set employees = new HashSet<>();
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Role role = (Role) o;
        return id.equals(role.id) && name.equals(role.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}
    

In the child entity, we just need to link the name of the property mapped in User entity.

“@JsonIgnore” annotation was placed there for employees property since I do not need to have the employee object to be seen in Role data.

This way we can establish a bi-directional many-to-many mapping! 💪

How data is saved in the 3rd table in our in-memory database is shown below, for this scenario.

When we fetch employees, it will give the result as expected.

That’s all guys! All the common relationship scenarios have been explained! 😍 According to the setup, uni or bi-directional mappings can be there. I have taken some real-world examples and tried to explain them all with code. We should always think from the aspect of real-world entities and how they practically exist. Then our lives will be easier while coding!!!

I hope you may get the idea and try to use ORM techniques while your day-to-day object-oriented programming. 💪

Bye bye ❤️

Original article by Salitha Chathuranga

Written by Salitha Chathuranga

Technical Lead at Sysco LABS | Senior Java Developer | Blogger