How To Define Ad-Hoc Entity Graphs In Spring Boot

Prateek
7 min readJun 14, 2021

--

In a nutshell, entity graphs (aka, fetch plans) is a feature introduced in JPA 2.1 that help us to improve the performance of loading entities. Mainly, we specify the entity’s related associations and basic fields that should be loaded in a single SELECT statement. We can define multiple entity graphs for the same entity and chain any number of entities and even use sub-graphs to create complex fetch plans. To override the current FetchType semantics there are properties that can be set:

  • Fetch Graph (default), javax.persistence.fetchgraph
    The attributes present in attributeNodes are treated as FetchType.EAGER. The remaining attributes are treated as FetchType.LAZY regardless of the default/explicit FetchType.
  • Load Graph, javax.persistence.loadgraph
    The attributes present in attributeNodes are treated as FetchType.EAGER. The remaining attributes are treated according to their specified or default FetchType.

Nevertheless, the JPA specs doesn’t apply in Hibernate for the basic (@Basic) attributes.. More details here.

Description: This is a sample application of defining ad-hoc entity graphs in Spring Boot.

Key points:

  • define two entities, Author and Book, involved in a lazy bidirectional @OneToMany relationship
  • the entity graph should load in a single SELECT the authors and the associatated books
  • in AuthorRepository rely on Spring @EntityGraph(attributePaths = {"books"}) annotation to indicate the ad-hoc entity graph

pom.xml

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Author.java

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class Author implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String genre;
private int age;

@OneToMany(cascade = CascadeType.ALL, mappedBy = "author", orphanRemoval = true)
private List<Book> books = new ArrayList<>();


public void addBook(Book book) {
this.books.add(book);
book.setAuthor(this);
}

public void removeBook(Book book) {
book.setAuthor(null);
this.books.remove(book);
}

public void removeBooks() {
Iterator<Book> iterator = this.books.iterator();

while (iterator.hasNext()) {
Book book = iterator.next();
book.setAuthor(null);
iterator.remove();
}
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getGenre() {
return genre;
}

public void setGenre(String genre) {
this.genre = genre;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public List<Book> getBooks() {
return books;
}

public void setBooks(List<Book> books) {
this.books = books;
}

@Override
public String toString() {
return "Author{" + "id=" + id + ", name=" + name
+ ", genre=" + genre + ", age=" + age + '}';
}
}

Book.java

import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

@Entity
public class Book implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String title;
private String isbn;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;


public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getIsbn() {
return isbn;
}

public void setIsbn(String isbn) {
this.isbn = isbn;
}

public Author getAuthor() {
return author;
}

public void setAuthor(Author author) {
this.author = author;
}

@Override
public boolean equals(Object obj) {

if (this == obj) {
return true;
}

if (getClass() != obj.getClass()) {
return false;
}

return id != null && id.equals(((Book) obj).id);
}

@Override
public int hashCode() {
return 2021;
}

@Override
public String toString() {
return "Book{" + "id=" + id + ", title=" + title + ", isbn=" + isbn + '}';
}

}

AuthorRepository.java

import com.bookstore.entity.Author;
import java.util.List;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
@Transactional(readOnly = true)
public interface AuthorRepository extends JpaRepository<Author, Long>, JpaSpecificationExecutor<Author>{

@EntityGraph(attributePaths = {"books"}, type = EntityGraph.EntityGraphType.FETCH)
@Override
public List<Author> findAll();

@EntityGraph(attributePaths = {"books"}, type = EntityGraph.EntityGraphType.FETCH)
public List<Author> findByAgeLessThanOrderByNameDesc(int age);

@EntityGraph(attributePaths = {"books"}, type = EntityGraph.EntityGraphType.FETCH)
public List<Author> findAll(Specification<Author> spec);

@EntityGraph(attributePaths = {"books"}, type = EntityGraph.EntityGraphType.FETCH)
@Query(value="SELECT a FROM Author a WHERE a.age > 20 AND a.age < 40")
public List<Author> fetchAllAgeBetween20And40();
}

BookRepository.java

import com.bookstore.entity.Book;
import java.util.List;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {

@Transactional(readOnly = true)
@EntityGraph(attributePaths = {"author"}, type = EntityGraph.EntityGraphType.FETCH)
@Override
public List<Book> findAll();
}

BookStoreService.java

@Service
public class BookstoreService {
@Autowired
private AuthorRepository authorRepository;
@Autowired
private BookRepository bookRepository;

public void displayAuthorsAndBooks() {
List<Author> authors = authorRepository.findAll();

for (Author author : authors) {
System.out.println("Author: " + author);
System.out.println("No of books: " + author.getBooks().size() + ", " + author.getBooks());
}
}

public void displayAuthorsAndBooksByAge(int age) {
List<Author> authors = authorRepository.findByAgeLessThanOrderByNameDesc(age);

for (Author author : authors) {
System.out.println("Author: " + author);
System.out.println("No of books: " + author.getBooks().size() + ", " + author.getBooks());
}
}

public void displayAuthorsAndBooksByAgeWithSpec() {
List<Author> authors = authorRepository.findAll(isAgeGt45());

for (Author author : authors) {
System.out.println("Author: " + author);
System.out.println("No of books: " + author.getBooks().size() + ", " + author.getBooks());
}
}

public void displayAuthorsAndBooksFetchAllAgeBetween20And40() {
List<Author> authors = authorRepository.fetchAllAgeBetween20And40();

for (Author author : authors) {
System.out.println("Author: " + author);
System.out.println("No of books: "+ author.getBooks().size() + ", " + author.getBooks());
}
}

public void displayBooksAndAuthors() {
List<Book> books = bookRepository.findAll();

for (Book book : books) {
System.out.println("Book: " + book);
System.out.println("Author: " + book.getAuthor());
}
}
}

AuthorSpecs.java

public class AuthorSpecs {

private static final int AGE = 45;

public static Specification<Author> isAgeGt45() {
return (Root<Author> root, CriteriaQuery<?> query, CriteriaBuilder builder)
-> builder.greaterThan(root.get("age"), AGE);
}
}

MainApplication.java

@SpringBootApplication
public class MainApplication {

@Autowired
private BookstoreService bookstoreService;

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

@Bean
public ApplicationRunner init() {
return args -> {
System.out.println("\nCall AuthorRepository#findAll():");
bookstoreService.displayAuthorsAndBooks();

System.out.println("\nCall AuthorRepository#findByAgeLessThanOrderByNameDesc(int age):");
bookstoreService.displayAuthorsAndBooksByAge(40);

System.out.println("\nCall AuthorRepository#findAll(Specification spec):");
bookstoreService.displayAuthorsAndBooksByAgeWithSpec();

System.out.println("\nCall AuthorRepository#fetchAllAgeBetween20And40():");
bookstoreService.displayAuthorsAndBooksFetchAllAgeBetween20And40();

System.out.println("\nCall BookRepository#findAll():");
bookstoreService.displayBooksAndAuthors();
};
}
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/bookstoredb?createDatabaseIfNotExist=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

spring.jpa.open-in-view=false

spring.datasource.initialization-mode=always
spring.datasource.platform=mysql

data-mysql.sql

insert into author (age, name, genre, id) values (23, "Mark Janel", "Anthology", 1); 
insert into author (age, name, genre, id) values (43, "Olivia Goy", "Horror", 2);
insert into author (age, name, genre, id) values (51, "Quartis Young", "Anthology", 3);
insert into author (age, name, genre, id) values (34, "Joana Nimar", "History", 4);
insert into author (age, name, genre, id) values (38, "Alicia Tom", "Anthology", 5);
insert into author (age, name, genre, id) values (56, "Katy Loin", "Anthology", 6);
insert into author (age, name, genre, id) values (23, "Wuth Troll", "Anthology", 7);

insert into book (isbn, title, author_id, id) values ("001-JN", "A History of Ancient Prague", 4, 1);
insert into book (isbn, title, author_id, id) values ("002-JN", "A People's History", 4, 2);
insert into book (isbn, title, author_id, id) values ("003-JN", "History Day", 4, 3);
insert into book (isbn, title, author_id, id) values ("001-MJ", "The Beatles Anthology", 1, 4);
insert into book (isbn, title, author_id, id) values ("001-OG", "Carrie", 2, 5);
insert into book (isbn, title, author_id, id) values ("002-OG", "House Of Pain", 2, 6);
insert into book (isbn, title, author_id, id) values ("001-AT", "Anthology 2000", 5, 7);

Console logs —

Call AuthorRepository#findAll():
Author: Author{id=1, name=Mark Janel, genre=Anthology, age=23}
No of books: 1, [Book{id=4, title=The Beatles Anthology, isbn=001-MJ}]
Author: Author{id=2, name=Olivia Goy, genre=Horror, age=43}
No of books: 2, [Book{id=5, title=Carrie, isbn=001-OG}, Book{id=6, title=House Of Pain, isbn=002-OG}]
Author: Author{id=3, name=Quartis Young, genre=Anthology, age=51}
No of books: 0, []
Author: Author{id=4, name=Joana Nimar, genre=History, age=34}
No of books: 3, [Book{id=1, title=A History of Ancient Prague, isbn=001-JN}, Book{id=2, title=A People’s History, isbn=002-JN}, Book{id=3, title=History Day, isbn=003-JN}]
Author: Author{id=5, name=Alicia Tom, genre=Anthology, age=38}
No of books: 1, [Book{id=7, title=Anthology 2000, isbn=001-AT}]
Author: Author{id=6, name=Katy Loin, genre=Anthology, age=56}
No of books: 0, []
Author: Author{id=7, name=Wuth Troll, genre=Anthology, age=23}
No of books: 0, []

Call AuthorRepository#findByAgeLessThanOrderByNameDesc(int age):
Author: Author{id=7, name=Wuth Troll, genre=Anthology, age=23}
No of books: 0, []
Author: Author{id=1, name=Mark Janel, genre=Anthology, age=23}
No of books: 1, [Book{id=4, title=The Beatles Anthology, isbn=001-MJ}]
Author: Author{id=4, name=Joana Nimar, genre=History, age=34}
No of books: 3, [Book{id=1, title=A History of Ancient Prague, isbn=001-JN}, Book{id=2, title=A People’s History, isbn=002-JN}, Book{id=3, title=History Day, isbn=003-JN}]
Author: Author{id=5, name=Alicia Tom, genre=Anthology, age=38}
No of books: 1, [Book{id=7, title=Anthology 2000, isbn=001-AT}]

Call AuthorRepository#findAll(Specification spec):
Author: Author{id=3, name=Quartis Young, genre=Anthology, age=51}
No of books: 0, []
Author: Author{id=6, name=Katy Loin, genre=Anthology, age=56}
No of books: 0, []

Call AuthorRepository#fetchAllAgeBetween20And40():
Author: Author{id=1, name=Mark Janel, genre=Anthology, age=23}
No of books: 1, [Book{id=4, title=The Beatles Anthology, isbn=001-MJ}]
Author: Author{id=4, name=Joana Nimar, genre=History, age=34}
No of books: 3, [Book{id=1, title=A History of Ancient Prague, isbn=001-JN}, Book{id=2, title=A People’s History, isbn=002-JN}, Book{id=3, title=History Day, isbn=003-JN}]
Author: Author{id=5, name=Alicia Tom, genre=Anthology, age=38}
No of books: 1, [Book{id=7, title=Anthology 2000, isbn=001-AT}]
Author: Author{id=7, name=Wuth Troll, genre=Anthology, age=23}
No of books: 0, []

Call BookRepository#findAll():
Book: Book{id=1, title=A History of Ancient Prague, isbn=001-JN}
Author: Author{id=4, name=Joana Nimar, genre=History, age=34}
Book: Book{id=2, title=A People’s History, isbn=002-JN}
Author: Author{id=4, name=Joana Nimar, genre=History, age=34}
Book: Book{id=3, title=History Day, isbn=003-JN}
Author: Author{id=4, name=Joana Nimar, genre=History, age=34}
Book: Book{id=4, title=The Beatles Anthology, isbn=001-MJ}
Author: Author{id=1, name=Mark Janel, genre=Anthology, age=23}
Book: Book{id=5, title=Carrie, isbn=001-OG}
Author: Author{id=2, name=Olivia Goy, genre=Horror, age=43}
Book: Book{id=6, title=House Of Pain, isbn=002-OG}
Author: Author{id=2, name=Olivia Goy, genre=Horror, age=43}
Book: Book{id=7, title=Anthology 2000, isbn=001-AT}
Author: Author{id=5, name=Alicia Tom, genre=Anthology, age=38}

--

--

Prateek
Prateek

Written by Prateek

Java Developer and enthusiast

No responses yet