JPA Entity Graph In Action

Himank Batra
Xebia Engineering Blog
5 min readMar 15, 2021
ER Diagram

Let’s start with an overview of the entity graph.

Entity Graph is introduced in JPA 2.1 for dealing with performance loading.

The definition of an entity graph is independent of the query and defines which attributes to fetch from the database. It allows you to define a template by grouping the related persistence fields which we want to retrieve and lets us choose the graph type at runtime.

Let's understand the concept in detail with a simple use of a BlogPost engine. The system will allow users to create their posts, while other users can comment on the same. The system will have the following entities :

Post

@Entity(name="POSTS")
@NoArgsConstructor
@Getter
public class Post {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String subject;
@OneToMany(mappedBy = "post")
private List<Comment> comments=new ArrayList<>();

@ManyToOne
@JoinColumn
private User user;

public Post(String subject, User user) {
this.subject = subject;
this.user = user;
}

}

User

@Entity(name="USERS")
@NoArgsConstructor
@Getter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;

public User(String name, String email) {
this.name = name;
this.email = email;
}
}

Comment

@Entity(name="COMMENTS")
@NoArgsConstructor
@Getter
public class Comment {

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

private String reply;

@ManyToOne
@JoinColumn
private Post post;

@ManyToOne
@JoinColumn
private User user;

public Comment(String reply, Post post, User user) {
this.reply = reply;
this.post = post;
this.user = user;
}

}

Following is the repository for Post entity:

PostRepository

public interface PostRepository extends JpaRepository<Post,Long> {

}

We will analyze queries generated by hibernate when we call findByid from PostRepository, by default OneToMany Mapping is Lazy and ManyToOne Mapping is Eager.

Let’s write a test for finding a post by id.

Setup for test

@BeforeAll
static void setup(@Autowired DataSource dataSource) throws SQLException {
try (Connection conn = dataSource.getConnection()) {
ScriptUtils.executeSqlScript(conn, new ClassPathResource("data-init.sql"));
}
}

data-init.sql

INSERT INTO `USERS` (`ID`,`NAME`,`EMAIL`) VALUES (1,'user1','user1@test.com');
INSERT INTO `USERS` (`ID`,`NAME`,`EMAIL`) VALUES (2,'user2','user2@test.com');
INSERT INTO `USERS` (`ID`,`NAME`,`EMAIL`) VALUES (3,'user3','user3@test.com');

INSERT INTO `POSTS` (`ID`,`SUBJECT`,`USER_ID`) VALUES (1,'JPA Entity Graph In Action',1);

INSERT INTO `COMMENTS` (`ID`,`REPLY`,`POST_ID`,`USER_ID`) VALUES (1,'Nice !!',1,2);
INSERT INTO `COMMENTS` (`ID`,`REPLY`,`POST_ID`,`USER_ID`) VALUES (2,'Cool !!',1,3);

As we have initialized the data, We should get a post with id 1.

PostRepositoryTests

@Test
void whenFindById_thenReturnPost() {

// when
Optional<Post> optionalPost = postRepository.findById(1L);
assertThat(optionalPost.isPresent()).isTrue();
Post found = optionalPost.get();
// then
assertThat
(found.getSubject())
.isEqualTo("JPA Entity Graph In Action");
assertThat(found.getComments()).hasSize(2);
}

What’s happening inside?

The method findById of postRepository returns an Optional<Post> which results in 1 select query. Hence we are asserting that a post should be present.

As comments of a post are loaded lazily, so in the last when we are calling getComments method of the Post entity, it will call 1 more select query to the database for finding comments of a post.

Below is the Test Result screenshot, we can see there are two queries generated.

query generated without entity graph

But this can be easily achieved by 1 query using join.

Different Approaches for this issue :

  1. Before JPA 2.1 we use FetchType Strategies to achieve this.

The FetchType method defines two strategies for fetching data from the database:

  • FetchType.EAGER: The persistence provider must load the related annotated field or property. This is the default behavior for @Basic, @ManyToOne, and @OneToOne annotated fields.
  • FetchType.LAZY: The persistence provider should load data when it’s first accessed, but can be loaded eagerly. This is the default behavior for @OneToMany, @ManyToMany, and @ElementCollection-annotated fields.

Unfortunately, this meta configuration is static and doesn’t allow to switch between these two strategies at runtime.

The main goal of the JPA Entity Graph is to improve the runtime performance when loading the entity’s related associations and basic fields.

2. Using Entity Graph

I have updated the Post entity and PostRepository.

I have defined the entity graph in the Post entity and referenced the entity graph in the PostRepository.

Let’s have a look at the updated code.

Post

@NamedEntityGraph(
name = "post-entity-graph",
attributeNodes = {
@NamedAttributeNode("subject"),
@NamedAttributeNode("user"),
@NamedAttributeNode("comments"),
}
)
@NamedEntityGraph(
name = "post-entity-graph-with-comment-users",
attributeNodes = {
@NamedAttributeNode("subject"),
@NamedAttributeNode("user"),
@NamedAttributeNode(value = "comments", subgraph = "comments-subgraph"),
},
subgraphs = {
@NamedSubgraph(
name = "comments-subgraph",
attributeNodes = {
@NamedAttributeNode("user")
}
)
}
)
@Entity(name = "POSTS")
@NoArgsConstructor
@Getter
public class Post {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String subject;
@JsonBackReference
@OneToMany(mappedBy = "post")
private List<Comment> comments=new ArrayList<>();

@ManyToOne
@JoinColumn
private User user;

public Post(String subject, User user) {
this.subject = subject;
this.user = user;
}

}

NamedEntityGraph has attributes name and attributeNodes where we can specify the name of the entity graph and fields needed for a specific operation.

We have used the entity graph `post-entity-graph` to define the fetch operations of the Post And Comment entity. If we want to do the same for the User entity, we can do this with an entity subgraph `post-entity-graph-with-comment-users`. The definition of a named subgraph is similar to the definition of a named entity graph and can be referenced as an attributeNode.

PostRepository

public interface PostRepository extends JpaRepository<Post,Long> {

// referencing a named entity graph
@EntityGraph(value = "post-entity-graph", type = EntityGraph.EntityGraphType.LOAD)
Optional<Post> findById(Long id);

// ad-hoc entity graph
@EntityGraph(attributePaths = { "subject","user" })
List<Post> getAllByUserId(Long id);

}

Let’s rerun the test which we have created before.

Here, in the below screenshot we can see that there is only 1 query.

query generated with entity graph

We can also create an ad-hoc entity graph by defining attributePaths in the EntityGraph annotation.

Types of Entity Graphs :

JPA defines two properties or hints by which the persistence provider can choose in order to load or fetch the Entity Graph at runtime:

  • javax.persistence.loadgraph — This property is used to specify an entity graph, attributes that are specified by attribute nodes of the entity graph are treated as FetchType.EAGER and attributes that are not specified are treated according to their specified or default FetchType.
  • javax.persistence.fetchgraph — This property is used to specify an entity graph, attributes that are specified by attribute nodes of the entity graph are treated as FetchType.EAGER and attributes that are not specified are treated as FetchType.LAZY.

It defaults to EntityGraph.EntityGraphType.FETCH.

In either case, the primary key and the version if any are always loaded.

Conclusion

The JPA provider loads all the graphs in one select query and then avoids fetching association with more SELECT queries. This is considered a good approach for improving application performance.

The JPA documentation recommends using the FetchType.LAZY strategy whenever possible, and the Entity Graph when we need to load an association.

You can find the code at my Github repository link.

--

--