1. Overview

JPA 2.1 has introduced the Entity Graph feature as a more sophisticated method of dealing with performance loading.

It allows defining a template by grouping the related persistence fields which we want to retrieve and lets us choose the graph type at runtime.

In this tutorial, we’ll explain in more detail how to create and use this feature.

2. What the Entity Graph Tries to Resolve

Until JPA 2.0, to load an entity association, we usually used *FetchType.*LAZY and FetchType.EAGER as fetching strategies. This instructs the JPA provider to additionally fetch the related association or not. Unfortunately, this meta configuration is static and doesn’t allow switching between these two strategies at runtime.

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

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

3. Defining the Model

Before we start exploring the Entity Graph, we need to define the model entities we’re working with. Let’s say we want to create a blog site where users can comment on and share posts.

So, first, we’ll have a User entity:

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

    //...
}

The user can share various posts, so we also need a Post entity:

@Entity
public class Post {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private User user;
    
    //...
}

The user can also comment on the shared posts, so, finally, we’ll add a Comment entity:

@Entity
public class Comment {

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

    private String reply;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private User user;
    
    //...
}

As we can see, the Post entity has an association with the Comment and User entities. The Comment entity has an association to the Post and User entities.

The goal is then to load the following graph using various ways:

Post  ->  user:User
      ->  comments:List<Comment>
            comments[0]:Comment -> user:User
            comments[1]:Comment -> user:User

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.

For example, when we load a Post entity, the related Comment entities are not loaded as the default FetchType since @OneToMany is LAZY. We can override this behavior by changing the FetchType to EAGER:

@OneToMany(mappedBy = "post", fetch = FetchType.EAGER)
private List<Comment> comments = new ArrayList<>();

By comparison, when we load a Comment entity, his Post parent entity is loaded as the default mode for @ManyToOne, which is EAGER. We can also choose to not load the Post entity by changing this annotation to LAZY:

@ManyToOne(fetch = FetchType.LAZY) 
@JoinColumn(name = "post_id") 
private Post post;

Note that as the LAZY is not a requirement, the persistence provider can still load the Post entity eagerly if it wants. So for using this strategy properly, we should go back to the official documentation of the corresponding persistence provider.

Now, because we’ve used annotations to describe our fetching strategy, our definition is static and there is no way to switch between the LAZY and EAGER at runtime.

This is where the Entity Graph come into play as we’ll see in the next section.

5. Defining an Entity Graph

To define an Entity Graph, we can either use the annotations on the entity or we can proceed programmatically using the JPA API.

5.1. Defining an Entity Graph with Annotations

The @NamedEntityGraph annotation allows specifying the attributes to include when we want to load the entity and the related associations.

So let’s first define an Entity Graph that loads the Post and his related entities User and Comments:

@NamedEntityGraph(
  name = "post-entity-graph",
  attributeNodes = {
    @NamedAttributeNode("subject"),
    @NamedAttributeNode("user"),
    @NamedAttributeNode("comments"),
  }
)
@Entity
public class Post {

    @OneToMany(mappedBy = "post")
    private List<Comment> comments = new ArrayList<>();
    
    //...
}

In this example, we’ve used the @NamedAttributeNode to define the related entities to be loaded when the root entity is loaded.

Let’s now define a more complicated Entity Graph where we want also load the Users related to the Comments.

For this purpose, we’ll use the @NamedAttributeNode subgraph attribute. This allows referencing a named subgraph defined through the @NamedSubgraph annotation:

@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
public class Post {

    @OneToMany(mappedBy = "post")
    private List<Comment> comments = new ArrayList<>();
    //...
}

The definition of the @NamedSubgraph annotation is similar to the @NamedEntityGraph and allows to specify attributes of the related association. Doing so, we can construct a complete graph.

In the example above, with the defined ‘post-entity-graph-with-comment-users’  graph, we can load the Post, the related User, the Comments and the Users related to the Comments.

Finally, note that we can alternatively add the definition of the Entity Graph using the orm.xml deployment descriptor:

<entity-mappings>
  <entity class="com.baeldung.jpa.entitygraph.Post" name="Post">
    ...
    <named-entity-graph name="post-entity-graph">
            <named-attribute-node name="comments" />
    </named-entity-graph>
  </entity>
  ...
</entity-mappings>

5.2. Defining an Entity Graph with the JPA API

We can also define the Entity Graph through the EntityManager API by calling the createEntityGraph() method:

EntityGraph<Post> entityGraph = entityManager.createEntityGraph(Post.class);

To specify the attributes of the root entity, we use the addAttributeNodes() method.

entityGraph.addAttributeNodes("subject");
entityGraph.addAttributeNodes("user");

Similarly, to include the attributes from the related entity, we use the addSubgraph() to construct an embedded Entity Graph and then we the addAttributeNodes() as we did above.

entityGraph.addSubgraph("comments")
  .addAttributeNodes("user");

Now that we have seen how to create the Entity Graph, we’ll explore how to use it in the next section.

6. Using the Entity Graph

6.1. 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:

  • jakarta.persistence.fetchgraph – Only the specified attributes are retrieved from the database. As we are using Hibernate in this tutorial, we can note that in contrast to the JPA specs, attributes statically configured as EAGER are also loaded.
  • jakarta.persistence.loadgraph – In addition to the specified attributes, attributes statically configured as EAGER are also retrieved.

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

6.2. Loading an Entity Graph

We can retrieve the Entity Graph using various ways.

Let’s start by using the EntityManager.find() method. As we’ve already shown, the default mode is based on the static meta-strategies FetchType.EAGER and FetchType.LAZY.

So let’s invoke the find() method and inspect the log:

Post post = entityManager.find(Post.class, 1L);

Here is the log provided by Hibernate implementation:

select
    post0_.id as id1_1_0_,
    post0_.subject as subject2_1_0_,
    post0_.user_id as user_id3_1_0_ 
from
    Post post0_ 
where
    post0_.id=?

As we can see from the log, the User and Comment entities are not loaded.

We can override this default behavior by invoking the overloaded find() method which accepts hints as a Map. We can then provide the graph type which we want to load:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph");
Map<String, Object> properties = new HashMap<>();
properties.put("jakarta.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);

If we look again in the log, we can see that these entities are now loaded and only in one select query:

select
    post0_.id as id1_1_0_,
    post0_.subject as subject2_1_0_,
    post0_.user_id as user_id3_1_0_,
    comments1_.post_id as post_id3_0_1_,
    comments1_.id as id1_0_1_,
    comments1_.id as id1_0_2_,
    comments1_.post_id as post_id3_0_2_,
    comments1_.reply as reply2_0_2_,
    comments1_.user_id as user_id4_0_2_,
    user2_.id as id1_2_3_,
    user2_.email as email2_2_3_,
    user2_.name as name3_2_3_ 
from
    Post post0_ 
left outer join
    Comment comments1_ 
        on post0_.id=comments1_.post_id 
left outer join
    User user2_ 
        on post0_.user_id=user2_.id 
where
    post0_.id=?

Let’s see how we can achieve the same thing using JPQL:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph-with-comment-users");
Post post = entityManager.createQuery("select p from Post p where p.id = :id", Post.class)
  .setParameter("id", id)
  .setHint("jakarta.persistence.fetchgraph", entityGraph)
  .getSingleResult();

And finally, let’s have a look at a Criteria API example:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph-with-comment-users");
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Post> criteriaQuery = criteriaBuilder.createQuery(Post.class);
Root<Post> root = criteriaQuery.from(Post.class);
criteriaQuery.where(criteriaBuilder.equal(root.<Long>get("id"), id));
TypedQuery<Post> typedQuery = entityManager.createQuery(criteriaQuery);
typedQuery.setHint("jakarta.persistence.loadgraph", entityGraph);
Post post = typedQuery.getSingleResult();

In each of these, the graph type is given as a hint. While in the first example, we used the Map, in the two later examples we’ve used the setHint() method.

7. Conclusion

In this article, we’ve explored using the JPA Entity Graph to dynamically fetch an Entity and its associations.

The decision is made at runtime in which we choose to load or not the related association.

Performance is obviously a key factor to take into account when designing JPA entities. The JPA documentation recommends using the FetchType.LAZY strategy whenever possible, and the Entity Graph when we need to load an association.

As usual, all the code is available on GitHub.


« 上一篇: Java Weekly, 第258期
» 下一篇: Java printf()格式化