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 20
package 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 5
spring.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 17
package 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
double
field 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 18
package 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 24
package 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 21
package 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 Employee
s. 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 31
package 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 26
package 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 Employee
s 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 40
package 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 23
package 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_id
and 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.