Using Spring and Hibernate to Map out relationships

Intro
Spring along with Hibernate is used in countless enterprise applications. Today we'll be learning how we can implement some standard database mappings using it. This guide will use MySQL as an example DB, but it would be trivial to port to any other relational database by just changing the dependencies and configuration in the Pom file.
Requirements
- Java 17
- A running MySQL instance. If you want to follow along exactly with this guide, the command below should set you up with a database running on port 3306
docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -p 3306:3306 -d mysql:tag
Boilerplate
We'll need some basic scaffolding for our project. Let's start by adding the usual folder structure, POM file and check our configuration is all working as expected. A full example of this project can be found at the Github repository here
Pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>com.jimmyblogs</groupId> <artifactId>spring-hibernate-example</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.27</version> </dependency> <!-- Compilation Dependencies--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>compile</scope> </dependency> </dependencies> </project>
Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20package com.jimmyblogs; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } @Bean public CommandLineRunner commandLineRunner(ApplicationContext ctx) { return args -> System.out.println("The application has started successfully!"); } }
application.properties
1 2 3 4 5spring.datasource.url=jdbc:mysql://127.0.0.1:3306/Jimmyblogs?createDatabaseIfNotExist=true spring.datasource.username=root spring.datasource.password=my-secret-pw spring.jpa.hibernate.ddl-auto=update spring.datasource.driver-class-name=com.mysql.jdbc.Driver
At this point we should be able to run the application from the Main method. If everything is working as expected, you should see the console message:
"The application has started successfully!"
Scenario
Let's create a scenario where we have been asked to design a Database for an office, and it's employees. Let's start with the initial Employee entity.
Employee.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17package com.jimmyblogs.entity; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import lombok.NoArgsConstructor; @NoArgsConstructor @Entity public class Employee { @Id @GeneratedValue Integer id; String name; Double salary; }
Adding this class, we are creating an Employee Entity with three columns.
- An Auto-Generated
(int)field called 'id' - A
varchar(255)field called 'name' - A
doublefield called 'salary'

If we run the application again, we should see the table get created automatically.
One-to-One mapping.
So let's create our first mapping.
A one-to-one mapping is where a column in one table has only one relationship with a column in another table.
Taking our Employee example, let's picture that one Employee is directly associated with one Client. And a single Client
is assigned exactly one Employee to look after them.
Let's implement this in Java, we're going to need a new Entity class called Client so let's create that first.
Client.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18package com.jimmyblogs.entity; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import lombok.NoArgsConstructor; @NoArgsConstructor @Entity public class Client { @Id @GeneratedValue Integer id; String name; Double contractAmount; }
If we were to run this application again, our second table client would be created but our tables are currently unrelated.

Let's add the mappings
Employee.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24package com.jimmyblogs.entity; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.OneToOne; import lombok.NoArgsConstructor; @NoArgsConstructor @Entity public class Employee { @Id @GeneratedValue Integer id; String name; Double salary; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "client_id", referencedColumnName = "id") Client client; }
Client.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21package com.jimmyblogs.entity; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToOne; import lombok.NoArgsConstructor; @NoArgsConstructor @Entity public class Client { @Id @GeneratedValue Integer id; String name; Double contractAmount; @OneToOne(mappedBy = "client") Employee employee; }
So let's take a look at what's changed here
On line 21 in the Employee class, we declare that we want to create a one-to-one mapping.
On line 22 we add the annotation @JoinColumn and specify which column we would like to map. In this case we are mapping
on the id column and a new column in our Client table will be created with the column name client_id
If all is working properly, you should end up with something that looks like this.

One-to-Many mapping.
One-to-many mappings are for more common, for this example we're going to use an example of a Department. An Employee can
only be a member of one Department, yet one Department can have many Employees. Therefore, this relationship is a one-to-many
relationship.
Let's edit our code to reflect this
Employee.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31package com.jimmyblogs.entity; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import lombok.NoArgsConstructor; @NoArgsConstructor @Entity public class Employee { @Id @GeneratedValue Integer id; String name; Double salary; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "client_id", referencedColumnName = "id") Client client; @ManyToOne @JoinColumn(name="department_id", nullable=false) Department department; }
Here we've added the @ManyToOne annotation on line 27 and told hibernate that we want to map the relationship by Department.
So in this case many employees can be related to one Department and one Department can be shared by many employees.
Department.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26package com.jimmyblogs.entity; import java.util.Set; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import lombok.NoArgsConstructor; @NoArgsConstructor @Entity public class Department { @Id @GeneratedValue Integer id; String name; String code; Double budget; @OneToMany(mappedBy="department") private Set<Employee> employees; }
In the Department class, we've added the @OneToMany annotation on line 22. We also specify that we wish to map this relationship by Department.
So in this case one Department can be related to many Employees as represented by the Set<Employee> field.
Our database should now be starting to look like this

Many to Many mapping
The final relationship type we will focus on are many-to-many relationships. In our contrived example, we're going to add
a requirement where we need to store the training courses that each Employee has been on. Each Employee can undertake
many training courses and each training Course can be undertaken by many employees. Therefore, we have a many-to-many
relationship between Course and Employee
Let's map that out by adapting our Employee entity as well as creating a new Course Entity
Employee.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40package com.jimmyblogs.entity; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.OneToOne; import lombok.NoArgsConstructor; @NoArgsConstructor @Entity public class Employee { @Id @GeneratedValue Integer id; String name; Double salary; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "client_id", referencedColumnName = "id") Client client; @ManyToOne @JoinColumn(name="department_id", nullable=false) Department department; @ManyToMany(cascade = { CascadeType.ALL }) @JoinTable( name = "employee_course", joinColumns = { @JoinColumn(name = "employee_id") }, inverseJoinColumns = { @JoinColumn(name = "course_id") } ) Set<Course> courses; }
Course.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23package com.jimmyblogs.entity; import java.util.Set; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToMany; import lombok.NoArgsConstructor; @NoArgsConstructor @Entity public class Course { @Id @GeneratedValue Integer id; String name; Integer duration; @ManyToMany(mappedBy = "courses") Set<Employee> employees; }
To create the relationship type of many-to-many, we're going to generate a junction table via Hibernate which will contain a mapping of each EmployeeId and CourseId. We do this within the Employee class;
- On line 35 we declare the junction table name of
employee_course. This is the name we will give our junction table - On line 36 we declare one column in the junction table with a foreign key to
employee_idand on line 37, another column with a foreign key tocourse_id.
Having a junction table like this means we can model the many-to-many relationship that exists between Employee and Course.
The Course class is simple as we have specified a junction table in the Employee class. For this we can simply tell hibernate to map our Course entities by our employees via the mappedBy property.
After running this, you should end up with something that looks the same as below

As you can see, a junction table is automatically created to model our many-to-many relationship between Employee and Course.
Conclusion
We have learnt various database relationship styles and how to implement them via hibernate.