JPA Entity Graph and the nuances of its use
Entity Graph is one of the powerful JPA tools that helps developers flexibly manage the loading of related entities. Entity Graph allows you to dynamically configure data loading at runtime, which makes it especially useful in projects with complex data structures.
Team Spring AIO I prepared an article in which I looked at how to use Entity Graph.
In JPA 2.1, a feature was added Entity Graphwhich improves the performance of loading related entities. Previously, in JPA 2.0, two approaches were used to manage data loading strategies: FetchType.LAZY And FetchType.EAGERHowever, these strategies are static, which does not allow flexible switching between them during program execution.
Of course it exists join fetchwhich allows you to turn lazy loading into greedy loading with the JOIN strategy. But the opposite method — to turn greedy into lazy — is not provided by JPQL. And here comes to the rescue @EntityGraph
! Let's figure out how to work with it in more detail…
Before we dive into Entity Graph, let's define the domain we're going to work with. Let's say we want to create a blog site where users can comment on posts and share them.
So, first let's declare an entity User
:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
The user can share different posts, so we also need an entity Post
:
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String subject;
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private User user;
}
The user can also comment on published messages, so let's add an entity Comment
:
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String reply;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Post post;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private User user;
}
Essence Post
has a connection with entities Comment
And User
. Essence Comment
has a connection with entities Post
And User
.
Our task is to load the graph:
Post -> user:User
-> comments:List<Comment>
comments[0]:Comment -> user:User
comments[1]:Comment -> user:User
Before Entity Graph, developers used FetchType strategies to control how related data was loaded:
– FetchType.EAGER: The entity is loaded together with its relationships (but not always in one query). This approach is used by default for annotations @ManyToOne
And @OneToOne
.
– FetchType.LAZY: Related data is loaded only when it is requested. This is the default behavior for relationships. @OneToMany
, @ManyToMany
And @ElementCollection
.
For example, when using LAZY for an entity Post
comments to the post (entity Comment
) will not be loaded by default:
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
At the same time for communication ManyToOne
default behavior – EAGER
which results in the automatic loading of related data:
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
However, to make loading more flexible and manageable at the execution level, Entity Graph is used.
Let's look at an example using @EntityGraph
and Spring Data:
@EntityGraph(attributePaths = {"comments"})
List<Post> findEntityGraphTypeFetchBySubject(String subject);
The graph will include loading all the basic fields of the user who created the post (user), as well as comments and their authors.
Hibernate:
select
p1_0.id,
c1_0.post_id,
c1_0.id,
c1_0.reply,
c1_0.user_id,
p1_0.subject,
p1_0.user_id
from
post p1_0
left join
comment c1_0
on p1_0.id=c1_0.post_id
where
p1_0.subject=?
It is worth noting that EntityGraph itself also has 2 types of loading: EntityGraph.EntityGraphType.FETCH
And EntityGraph.EntityGraphType.LOAD
. When Fetch mode is selected (used by default), associative attributes that are explicitly declared for loading, such as comments in our case, will be fetched greedily (FetchType.EAGER), while other attributes will be loaded lazily (FetchType.LAZY).
In case we use the mode EntityGraph.EntityGraphType.LOAD
, selected attributes will be loaded eagerly, and the rest of the attributes will be loaded according to the FetchType specified in the model.
@EntityGraph(attributePaths = {"comments"}, type = EntityGraph.EntityGraphType.LOAD)
List<Post> findEntityGraphTypeLoadBySubject(String subject);
Hibernate:
select
p1_0.id,
c1_0.post_id,
c1_0.id,
c1_0.reply,
c1_0.user_id,
p1_0.subject,
p1_0.user_id
from
post p1_0
left join
comment c1_0
on p1_0.id=c1_0.post_id
where
p1_0.subject=?
Hibernate:
select
u1_0.id,
u1_0.email,
u1_0.name
from
user_ u1_0
where
u1_0.id=?
Hibernate:
select
u1_0.id,
u1_0.email,
u1_0.name
from
user_ u1_0
where
u1_0.id=?
//И так далее, N+1 запрос
As you can see, all user information is loaded as specified in the model – that is, greedily, even though we did not specify the user field for @EntityGraph.
Separately, we note that the basic attributes will always be loaded, regardless of the selected loading mode, whether it be FETCH or LOAD. Therefore, there is no point in specifying them as values for attributePaths.
Join the Russian-speaking Spring Boot developer community in telegram – Spring AIOto stay up to date with the latest news from the world of Spring Boot development and everything related to it.
We are waiting for everyone, join us