Hibernate LazyToOne annotation
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 the Hibernate LazyToOne annotation works and why you should use NO_PROXY
lazy loading with bytecode enhancement.
Before Hibernate 5.5, without the LazyToOneOption.NO_PROXY
annotation, the parent-side of a @OneToOne
association is always going to be fetched eagerly even if you set it to FetchType.LAZY
and enabled bytecode enhancement lazy loading.
Since Hibernate 5.5, you no longer need to use
LazyToOneOption.NO_PROXY
with bytecode enhancement.
The Hibernate LazyToOne annotation and the LazyToOneOption enumeration
The Hibernate @LazyToOne
annotation looks as follows:
The value
attribute takes a LazyToOneOption
enumeration, which provides one of the following three values:
public enum LazyToOneOption { FALSE, PROXY, NO_PROXY }
Next, we will see how all these three options work with JPA and Hibernate.
LazyToOneOption.FALSE Hibernate annotation
If you are using the LazyToOneOption.FALSE
, an association will be fetched eagerly even if it’s using the FetchType.LAZY
fetch strategy.
So, considering we have the following parent Post
entity:
@Entity(name = "Post") @Table(name = "post") public class Post { @Id private Long id; private String title; //Getters and setters omitted for brevity }
And the following client PostDetails
entity that defines a one-to-one
association using @MapsId
to share the identifier with its parent Post
entity:
@Entity(name = "PostDetails") @Table(name = "post_details") public class PostDetails { @Id @GeneratedValue private Long id; @Column(name = "created_on") private Date createdOn = new Date(); @Column(name = "created_by") private String createdBy; @OneToOne(fetch = FetchType.LAZY) @LazyToOne(LazyToOneOption.FALSE) @MapsId @JoinColumn(name = "id") private Post post; //Getters and setters omitted for brevity }
Notice that the fetch
attribute of the @OneToOne
annotation is set to FetchType.LAZY
.
However, we also have the @LazyToOne(LazyToOneOption.FALSE)
annotation set on the post
association.
If we add the following Post
and PostDetails
entities:
final Post post = new Post() .setId(1L) .setTitle("High-Performance Java Persistence, 1st Part"); doInJPA(entityManager -> { entityManager.persist(post); entityManager.persist( new PostDetails() .setPost(post) .setCreatedBy("Vlad Mihalcea") ); });
If we want to fetch the PostDetails
entity:
PostDetails details = doInJPA(entityManager -> { return entityManager.find(PostDetails.class, post.getId()); }); assertNotNull(details.getPost());
We would expect to have the post
association represented by an uninitialized Proxy, but that’s not going to be the case. Instead, Hibernate executes 2 SQL queries:
SELECT pd.id AS id1_1_0_, pd.created_by AS created_2_1_0_, pd.created_on AS created_3_1_0_ FROM post_details pd WHERE pd.id = 1 SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
The first SQL query is the one we expected. The second one that fetches the Post
entity eagerly was executed because we annotated the post
association with the @LazyToOne(LazyToOneOption.FALSE)
annotation.
Behind the scenes, this is how the @LazyToOne
annotation is being interpreted by Hibernate:
LazyToOne lazy = property.getAnnotation(LazyToOne.class); if ( lazy != null ) { toOne.setLazy( !(lazy.value() == LazyToOneOption.FALSE) ); toOne.setUnwrapProxy( (lazy.value() == LazyToOneOption.NO_PROXY) ); }
The
@LazyToOne(LazyToOneOption.FALSE)
Hibernate annotation operates as if you set thefetch
strategy toFetchType.EAGER
.
LazyToOneOption.PROXY Hibernate annotation
If we switch the LazyToOneOption
value from FALSE
to PROXY
, as illustrated by the following example:
@OneToOne(fetch = FetchType.LAZY) @LazyToOne(LazyToOneOption.PROXY) @MapsId @JoinColumn(name = "id") private Post post;
And we fetch the PostDetails
entity:
PostDetails details = doInJPA(entityManager -> { return entityManager.find(PostDetails.class, post.getId()); }); assertNotNull(details.getPost()); LOGGER.info("Post entity class: {}", details.getPost().getClass());
We can see that a single SQL query is executed this time:
SELECT pd.id AS id1_1_0_, pd.created_by AS created_2_1_0_, pd.created_on AS created_3_1_0_ FROM post_details pd WHERE pd.id = 1
And the class of the Post
entity reference is HibernateProxy
:
-- Post entity class: Post$HibernateProxy$QrlX9iOq
This is the default behavior for FetchType.LAZY
associations, so we get the same outcome even if we omit the @LazyToOne(LazyToOneOption.PROXY)
annotation.
The
@LazyToOne(LazyToOneOption.PROXY)
Hibernate annotation is redundant if the association uses theFetchType.LAZY
strategy.
LazyToOneOption.NO_PROXY Hibernate annotation
To understand where the LazyToOneOption.NO_PROXY
annotation is useful, let’s change the previous @OneToOne
association from unidirectional to bidirectional. So, while the PostDetails
mapping stays the same, the parent Post
entity will feature a details
property as well:
So, the Post
entity mapping looks as follows:
@Entity(name = "Post") @Table(name = "post") public class Post { @Id private Long id; private String title; @OneToOne( mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL ) private PostDetails details; //Getters and setters omitted for brevity }
As explained in this article, the parent side of a @OneToOne
association is always fetched eagerly even if it’s set to FetchType.LAZY
.
So, when fetching the Post
entity:
Post post = doInJPA(entityManager -> { return entityManager.find(Post.class, 1L); });
Hibernate is going to execute two SQL queries instead of just one:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1 SELECT pd.id AS id1_1_0_, pd.created_by AS created_2_1_0_, pd.created_on AS created_3_1_0_ FROM post_details pd WHERE pd.id = 1
And, when inspecting the post
entity, we can see that the details
association is fetched even if we set it to FetchType.LAZY
:
post = {Post@5438} id = {Long@5447} 1 title = "High-Performance Java Persistence, 1st Part" details = {PostDetails@5449} id = {Long@5447} 1 createdOn = {Timestamp@5452} "2021-01-06 15:35:18.708" createdBy = "Vlad Mihalcea" post = {Post@5438}
This is undesirable since if we fetch N Post
entities without needing to fetch their associated details
associations, Hibernate will execute N additional SQL queries, leading to an N+1 query issue.
So, to avoid this issue, we need to enable bytecode enhancement lazy loading:
<plugin> <groupId>org.hibernate.orm.tooling</groupId> <artifactId>hibernate-enhance-maven-plugin</artifactId> <version>${hibernate.version}</version> <executions> <execution> <configuration> <enableLazyInitialization>true</enableLazyInitialization> </configuration> <goals> <goal>enhance</goal> </goals> </execution> </executions> </plugin>
However, this is not sufficient. We also need to annotate the details
property with @LazyToOne(LazyToOneOption.NO_PROXY)
:
@OneToOne( mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL ) @LazyToOne(LazyToOneOption.NO_PROXY) private PostDetails details;
Now, when fetching the parent Post
entity, we can see that a single SQL query is generated:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
And, the Post
entity is fetched as follows:
post = {Post@5475} id = {Long@5484} 1 title = "High-Performance Java Persistence, 1st Part" details = null $$_hibernate_entityEntryHolder = null $$_hibernate_previousManagedEntity = null $$_hibernate_nextManagedEntity = null $$_hibernate_attributeInterceptor = {LazyAttributeLoadingInterceptor@5486}
The $$_hibrnate_
properties are injected by the bytecode enhancement mechanism, and the $$_hibernate_attributeInterceptor
is responsible for intercepting the getter method calls and initializing the details
proxy on demand.
For Hibernate 5.4 or older versions, without the
@LazyToOne(LazyToOneOption.NO_PROXY)
annotation, thedetails
association would be fetched eagerly even if the bytecode enhancement lazy loading mechanism is enabled.If you migrated to Hibernate 5.5 or a newer version, the
@LazyToOne(LazyToOneOption.NO_PROXY)
annotation is obsolete, and you should not use it when enabling bytecode enhancement lazy loading.
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
The LazyToOne
Hibernate annotation allows us to control the fetching strategy beyond the default FetchType
options. While the FALSE
and PROXY
options are rarely needed, the NO_PROXY
option is very useful when using bidirectional @OneToOne
associations.
Without using the Hibernate @LazyToOne(LazyToOneOption.NO_PROXY)
annotation for HIbernate 5.4 or older versions, the parent side of a bidirectional @OneToOne
association will use a FetchType.EAGER
strategy even if we explicitly marked it with FetchType.LAZY
and enabled the bytecode enhancement lazy loading.
If you’re using HIbernate 5.5 or a newer version, then you no longer need to use the @LazyToOne(LazyToOneOption.NO_PROXY)
annotation.
