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 inattributeNodes
are treated asFetchType.EAGER
. The remaining attributes are treated asFetchType.LAZY
regardless of the default/explicitFetchType
. - Load Graph,
javax.persistence.loadgraph
The attributes present inattributeNodes
are treated asFetchType.EAGER
. The remaining attributes are treated according to their specified or defaultFetchType
.
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
andBook
, 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}