JPA Entity Graph
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
In this article, I’m going to explain how you can fetch an entity association using a JPA Entity Graph and how you can build it either declaratively or programmatically.
JPA FetchType
A JPA association can be fetched lazily or eagerly. The fetching strategy is controlled via the fetch
attribute of the @OneToMany
, @OneToOne
, @ManyToOne
, or @ManyToMany
.
The fetch
attribute can be either FetchType.LAZY
or FetchType.EAGER
. By default, @OneToMany
and @ManyToMany
associations use the FetchType.LAZY
strategy while the @OneToOne
and @ManyToOne
use the FetchType.EAGER
strategy instead.
As I explained in this article, the FetchType.EAGER
strategy is terrible default. Never in my life, I’ve seen a good use case that required an association to use the FetchType.EAGER
strategy. That’s because it’s unlikely that every possible business use case will require fetching a given association and the fact that Hibernate cannot override the FetchType.EAGER
strategy with FetchType.LAZY
at query execution time.
Default Fetch Plan
As I explained in this article, every entity has a default fetch plan that’s defined during entity mapping and instructs Hibernate how to fetch entity associations.
By default, @ManyToOne
and @OneToOne
associations use the FetchTyp.EAGER
strategy, which is a terrible choice from a performance perspective. So, for this reason, it’s good practice to set all @ManyToOne
and @OneToOne
associations to use the FetchType.LAZY
strategy, like in the following example:
@Entity(name = "PostComment") @Table(name = "post_comment") public class PostComment { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) private Post post; private String review; //Getters and setters omitted for brevity }
When fetching the PostComment
entity using the find
method:
PostComment comment = entityManager.find(PostComment.class, 1L);
Hibernate executes the following SQL query:
SELECT pc.id AS id1_1_0_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_ FROM post_comment pc WHERE pc.id = 1
The post
association is fetched as a Proxy that has only the id
set by the post_id
Foreign Key column that was loaded by the aforementioned SQL query.
When accessing any non-id property of the post
Proxy:
LOGGER.info("The comment post title is '{}'", comment.getPost().getTitle());
A secondary SQL query is executed that fetched the Post
entity on-demand:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1 -- The comment post title is 'High-Performance Java Persistence, part 1'
Overriding the default Fetch Plan
If we want to override the default fetch plan and fetch the post
association eagerly at query execution time, we can use a JPQL query that instructs Hibernate to fetch the lazy association using the FETCH JOIN clause:
PostComment comment = entityManager.createQuery(""" select pc from PostComment pc left join fetch pc.post where pc.id = :id """, PostComment.class) .setParameter("id", 1L) .getSingleResult(); LOGGER.info("The comment post title is '{}'", comment.getPost().getTitle());
Then, the default fetch plan is going to be overridden, and the post
association will be fetched eagerly:
SELECT pc.id AS id1_1_0_, p.id AS id1_0_1_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_, p.title AS title2_0_1_ FROM post_comment pc LEFT JOIN post p ON pc.post_id = p.id WHERE pc.id = 1
Declarative JPA Entity Graph
The default fetch plan can also be overridden using a JPA Entity Graph. For instance, we could define a specific fetch plan using the following JPA @EntityGraph
annotation:
@Entity(name = "PostComment") @Table(name = "post_comment") @NamedEntityGraph( name = "PostComment.post", attributeNodes = @NamedAttributeNode("post") ) public class PostComment { //Code omitted for brevity }
With the PostComment.post
Entity Graph in place, we can now load the PostComment
entity along with its associated post
entity, like this:
PostComment comment = entityManager.find( PostComment.class, 1L, Collections.singletonMap( "javax.persistence.loadgraph", entityManager.getEntityGraph("PostComment.post") ) );
And, when executing the above find
method, Hibernate generates the following SQL SELECT query:
SELECT pc.id AS id1_1_0_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_, p.id AS id1_0_1_, p.title AS title2_0_1_ FROM post_comment pc LEFT OUTER JOIN post p ON pc.post_id = p.id WHERE pc.id = 1
If you’re using Spring, then you can reference the JPA Entity Graph in a Repository method using the @EntityGraph
annotation:
@Repository public interface PostCommentRepository extends CrudRepository<PostComment, Long> { @EntityGraph( value = "PostComment.post", type = EntityGraphType.LOAD ) PostComment findById(Long id); }
Programmatic JPA Entity Graph
If you don’t like annotations, then you can also build the JPA Entity Graph programmatically, using the createEntityGraph
method of the JPA EntityManager
, as illustrated by the following example:
EntityGraph<PostComment> postCommentGraph = entityManager .createEntityGraph(PostComment.class); postCommentGraph.addAttributeNodes("post"); PostComment comment = entityManager.find( PostComment.class, 1L, Collections.singletonMap( "javax.persistence.loadgraph", postCommentGraph ) );
Awesome, right?
I'm running an online workshop on the 11th of October about High-Performance SQL.If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
In this article, you learned how the default fetch plan works and how you can override it using either a JPQL query or a JPA Entity Graph.
The JPA Entity Graph can be built declaratively using the JPA @NamedEntityGraph
annotation or programmatically via the createEntityGraph
method.
