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:

Hibernate LazyToOne annotation

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 the fetch strategy to FetchType.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 the FetchType.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:

Hibernate LazyToOne NO_PROXY usage

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, the details 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.

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

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.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.