Spring Data JPA Entity Graphs

One of the important features in Spring Data JPA or simply JPA is the ability to specify fetch plans using Entity Graphs. This is useful since it allows you to customize the data, which is retrieved with a query or find operation. It is expected to display data from the same entity in different and several ways when working with mid to large size applications. In other cases, you just need to select a smallest set of information to optimize the performance of your application.

Typically you do not have many mechanisms to control what is loaded or what is not loaded in a JPA Entity. You could use EAGER/LAZY fetching, but these definitions are pretty much static. You are unable to change their behaviour at runtime when retrieving data, meaning that you are stuck with what was defined in the entity. Changing these amid development is a nightmare, since it can cause queries to behave unexpectedly. Another way to control loading is to write specific JPQL queries.

When an object is retrieved from the datastore by JPA typically not all fields are retrieved immediately. This is because for efficiency purposes only particular field types are retrieved in the initial access of the object, and then any other objects are retrieved when required (lazy loading). The group of fields that are loaded is called an entity graph.

There are three types of “entity graphs”

  • Default Entity Graph: implicitly defined in all JPA specs, specifying the fetch setting for each field/property (LAZY/EAGER).
  • Named Entity Graphs: a new feature in JPA allowing the user to define Named Entity Graphs in metadata, via annotations or XML
  • Unnamed Entity Graphs: a new feature in JPA allowing the user to define Entity Graphs via the JPA API at runtime

MySQL Table

I will create two tables department and employee under roytuts database in MySQL server to implement the entity graph example using Spring Data JPA.

CREATE TABLE `department` (
  `dept_id` int unsigned NOT NULL AUTO_INCREMENT,
  `dept_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`dept_id`)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `employee` (
  `emp_id` int unsigned NOT NULL AUTO_INCREMENT,
  `emp_first_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
  `emp_last_name` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
  `emp_mgr_id` int DEFAULT NULL,
  `emp_designation` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `dept_id` int unsigned NOT NULL,
  PRIMARY KEY (`emp_id`),
  KEY `fk_emp_dept` (`dept_id`),
  CONSTRAINT `fk_emp_dept` FOREIGN KEY (`dept_id`) REFERENCES `department` (`dept_id`)
) ENGINE=InnoDB AUTO_INCREMENT=7935 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

I am also storing some sample data to test the application after the coding is done.

insert  into `department`(`dept_id`,`dept_name`) values (10,'ACCOUNTING'),(20,'RESEARCH'),(30,'SALES'),(40,'OPERATIONS');

insert  into `employee`(`emp_id`,`emp_first_name`,`emp_last_name`,`emp_mgr_id`,`emp_designation`,`dept_id`)
values (7369,'SMITH','JHON',7782,'CLERK',20),
(7499,'ALLEN','BORDER',7698,'SALESMAN',30),
(7521,'WARD','SPACE',7698,'SALESMAN',30),
(7654,'MARTIN','FOWLER',7698,'SALESMAN',30),
(7698,'BLAKE','RAY',NULL,'MANAGER',30),
(7782,'CLARK','MICHAEL',NULL,'MANAGER',10),
(7788,'SCOTT','TIGER',7566,'ANALYST',20),
(7839,'KING','ROY',NULL,'VICE PRESIDENT',10),
(7844,'TURNER','RICK',7698,'SALESMAN',30),
(7876,'ADAMS','EVE',7788,'CLERK',20),
(7900,'JAMES','BOND',7698,'CLERK',30),
(7902,'FORD','LAMBDA',7782,'ANALYST',20),
(7934,'MILLER','JOHN',7782,'CLERK',10);

Project Setup

I am going to create a gradle based project in Eclipse. You can use your favorite IDE or tool. The name of the project is spring-data-jpa-entity-graphs.

If you are creating gradle based project then use below build.gradle script:

buildscript {
	ext {
		springBootVersion = '2.3.2.RELEASE'
	}
	
    repositories {
    	mavenCentral()
    }
    
    dependencies {
    	classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

plugins {
    id 'java-library'
    id 'org.springframework.boot' version "${springBootVersion}"
}

sourceCompatibility = 12
targetCompatibility = 12

repositories {
    mavenCentral()
}

dependencies {
	implementation "org.springframework.boot:spring-boot-starter:${springBootVersion}"
    implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
    runtime("mysql:mysql-connector-java:8.0.17")
	
	//required for jdk 9 or above
	runtimeOnly('javax.xml.bind:jaxb-api:2.4.0-b180830.0359')
}

If you are creating maven based project then you can use below pom.xml file:

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

	<groupId>com.roytuts</groupId>
	<artifactId>spring-data-jpa-entity-graphs</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.2.RELEASE</version>
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		
		<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.17</version>
		</dependency>
		
		<!--required only if jdk 9 or higher version is used-->
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.4.0-b180830.0359</version>
		</dependency>
	</dependencies>

    <build>
        <plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>at least 8</source>
					<target>at least 8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

Database Configuration

Now I am going to create application.properties file under src/main/resources folder to setup the database details.

spring.datasource.url=jdbc:mysql://localhost/roytuts
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver

#disable schema generation from Hibernate
spring.jpa.hibernate.ddl-auto=none

The corresponding Java configuration class is given below for creating DataSource bean:

package com.roytuts.spring.data.jpa.entity.graphs.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

@Configuration
@PropertySource("classpath:application.properties")
public class Config {

	@Autowired
	private Environment environment;

	@Bean
	public DataSource dataSource() {

		DriverManagerDataSource ds = new DriverManagerDataSource();
		ds.setDriverClassName(environment.getRequiredProperty("spring.datasource.driverClassName"));
		ds.setUrl(environment.getRequiredProperty("spring.datasource.url"));
		ds.setUsername(environment.getRequiredProperty("spring.datasource.username"));
		ds.setPassword(environment.getRequiredProperty("spring.datasource.password"));
		return ds;
	}

}

Entity Classes

Entity classes are required to map Java class with database table. The fields or attributes of the classes are used to map with table column names.

package com.roytuts.spring.data.jpa.entity.graphs.entity;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "employee")
public class Employee implements Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "emp_id")
	private Integer empId;

	@Column(name = "emp_first_name")
	private String empFirstName;

	@Column(name = "emp_last_name")
	private String empLastName;

	@Column(name = "emp_mgr_id")
	private Integer empMgrId;

	@Column(name = "emp_designation")
	private String empDesignation;

	@JoinColumn(name = "dept_id", referencedColumnName = "dept_id")
	@ManyToOne(optional = false)
	private Department deptId;

	public Employee() {
	}

	public Employee(Integer empId) {
		this.empId = empId;
	}

	public Employee(Integer empId, String empFirstName, String empLastName) {
		this.empId = empId;
		this.empFirstName = empFirstName;
		this.empLastName = empLastName;
	}

	//getters and setters

	@Override
	public int hashCode() {
		int hash = 0;
		hash += (empId != null ? empId.hashCode() : 0);
		return hash;
	}

	@Override
	public boolean equals(Object object) {
		if (!(object instanceof Employee)) {
			return false;
		}
		Employee other = (Employee) object;
		if ((this.empId == null && other.empId != null) || (this.empId != null && !this.empId.equals(other.empId))) {
			return false;
		}
		return true;
	}

	@Override
	public String toString() {
		return "Employee [empId=" + empId + ", empFirstName=" + empFirstName + ", empLastName=" + empLastName
				+ ", empMgrId=" + empMgrId + ", empDesignation=" + empDesignation + ", deptId=" + deptId + "]";
	}

}
package com.roytuts.spring.data.jpa.entity.graphs.entity;

import java.io.Serializable;
import java.util.Collection;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedAttributeNode;
import javax.persistence.NamedEntityGraph;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name = "department")
@NamedEntityGraph(name = "department.employees", attributeNodes = { @NamedAttributeNode("employeeCollection") })
public class Department implements Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "dept_id")
	private Integer deptId;

	@Column(name = "dept_name")
	private String deptName;

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "deptId")
	private Collection<Employee> employeeCollection;

	public Department() {
	}

	//getters and setters

	@Override
	public int hashCode() {
		int hash = 0;
		hash += (deptId != null ? deptId.hashCode() : 0);
		return hash;
	}

	@Override
	public boolean equals(Object object) {
		if (!(object instanceof Department)) {
			return false;
		}
		Department other = (Department) object;
		if ((this.deptId == null && other.deptId != null)
				|| (this.deptId != null && !this.deptId.equals(other.deptId))) {
			return false;
		}
		return true;
	}

	@Override
	public String toString() {
		return "Department [deptId=" + deptId + ", deptName=" + deptName + "]";
	}

}

Here in the above Department entity class, we have NamedEntityGraph to include employeeCollection field loaded when Department object is loaded.

Spring Data JPA Repository

Spring provides built-in methods for performing basic CRUD operations. If you need any other operation or join operations then you need to write your Query method.

package com.roytuts.spring.data.jpa.entity.graphs.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.roytuts.spring.data.jpa.entity.graphs.entity.Employee;

public interface EmployeeRepository extends JpaRepository<Employee, Integer> {

}
package com.roytuts.spring.data.jpa.entity.graphs.repository;

import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
import org.springframework.data.jpa.repository.JpaRepository;

import com.roytuts.spring.data.jpa.entity.graphs.entity.Department;

public interface DepartmentRepository extends JpaRepository<Department, Integer> {

	@EntityGraph(value = "department.employees", type = EntityGraphType.LOAD)
	Department findByDeptId(int deptId);

}

Here in the above DepartmentRepository, I want to fetch the department by department id and also I want to fetch all employees for this department using Entity Graph.

Spring Service Class

Generally service class is responsible for performing business logic for the application.

package com.roytuts.spring.data.jpa.entity.graphs.service;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;

import com.roytuts.spring.data.jpa.entity.graphs.entity.Department;
import com.roytuts.spring.data.jpa.entity.graphs.repository.DepartmentRepository;

@Service
public class DepartmentService {

	@Resource
	private DepartmentRepository departmentRepository;

	public Department getDepartmentById(int deptId) {
		return departmentRepository.findByDeptId(deptId);
	}

}

Main Class

Finally I am going to create a class with main method with @SpringBootApplication, which will deploy the application into embedded Tomcat server.

package com.roytuts.spring.data.jpa.entity.graphs;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.roytuts.spring.data.jpa.entity.graphs.entity.Department;
import com.roytuts.spring.data.jpa.entity.graphs.service.DepartmentService;

@SpringBootApplication
public class SpringDataJpaEntityGraphApp implements CommandLineRunner {

	@Autowired
	private DepartmentService departmentService;

	public static void main(String[] args) {
		SpringApplication.run(SpringDataJpaEntityGraphApp.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		Department department = departmentService.getDepartmentById(10);

		System.out.println("Department name: " + department);
		department.getEmployeeCollection().stream().forEach(emp -> System.out.println(emp));
	}

}

Testing the Application

Running the above class will give you the following output in the console.

Department name: Department [deptId=10, deptName=ACCOUNTING]
Employee [empId=7782, empFirstName=CLARK, empLastName=MICHAEL, empMgrId=null, empDesignation=MANAGER, deptId=Department [deptId=10, deptName=ACCOUNTING]]
Employee [empId=7839, empFirstName=KING, empLastName=ROY, empMgrId=null, empDesignation=VICE PRESIDENT, deptId=Department [deptId=10, deptName=ACCOUNTING]]
Employee [empId=7934, empFirstName=MILLER, empLastName=JOHN, empMgrId=7782, empDesignation=CLERK, deptId=Department [deptId=10, deptName=ACCOUNTING]]

That’s all.

Source Code

Download

Thanks for reading.

Leave a Reply

Your email address will not be published. Required fields are marked *