Hibernate LazyToOne annotation
Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?
What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?
Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.
So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!
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_PROXYwith 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 thefetchstrategy 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.LAZYstrategy.
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, thedetailsassociation 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.






