How to lazy load entity properties with Hibernate

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

One of my readers bumped into the JSON mapping post and asked me if we can fetch the JSON properties lazily. This post demonstrates how easily this can be done when using Hibernate as a JPA provider.

As I previously explained, EAGER fetching is a code smell and loading associations eagerly is very detriment to application performance. However, it’s not just associations that we must be careful about. Basic entity properties may also cause performance issues as well. In this post, I’m going to show you how you can fetch entity properties lazily.

Bytecode enhancement

By default, Hibernate loads all properties eagerly. So if your entity has many properties or the underlying columns are relatively large, there is going to be a considerable performance impact upon fetching the entity in question. While associations can be loaded lazily using runtime-generated proxies, for basic properties we need bytecode enhancement.

To enhance our entities with basic property lazy loading capabilities, the enableLazyInitialization configuration property must be explicitly set to true:

<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>

Domain Model

I’m going to reuse the same Domain Model used in the JSON mapping post. Basically, we want to fetch all the JSON properties lazily because the JSON object size might be quite large.

The Event entity has a location property that stores a JSON object. To load it lazily, we need to mark this property with the @Basic(fetch = FetchType.LAZY) annotation:

@Entity(name = "Event")
@Table(name = "event")
public class Event extends BaseEntity {

    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    @Basic(fetch = FetchType.LAZY)
    private Location location;

    public Location getLocation() {
        return location;
    }

    public void setLocation(Location location) {
        this.location = location;
    }
}

The Participant entity has a ticket property that stores a JSON object, therefore this property is annotated with the with @Basic(fetch = FetchType.LAZY) as well:

@Entity(name = "Participant")
@Table(name = "participant")
public class Participant extends BaseEntity {

    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    @Basic(fetch = FetchType.LAZY)
    private Ticket ticket;

    @ManyToOne(fetch = FetchType.LAZY)
    private Event event;

    public Ticket getTicket() {
        return ticket;
    }

    public void setTicket(Ticket ticket) {
        this.ticket = ticket;
    }

    public Event getEvent() {
        return event;
    }

    public void setEvent(Event event) {
        this.event = event;
    }
}

Testing time

To prove that property lazy loading works, we are going to fetch the Event entity and navigate the location property as follows:

Event event = entityManager.find(Event.class, 
    eventHolder.get().getId());

LOGGER.debug("Fetched event");
assertEquals("Cluj-Napoca", event.getLocation().getCity());

When running the test case above, Hibernate generates the following statements:

SELECT e.id AS id1_0_0_
FROM   event e
WHERE  e.id = 1

-- Fetched event

SELECT e.location AS location2_0_
FROM   event e
WHERE  e.id = 1

As you can see, the first query does not retrieve the location property, which is only initialized when being navigated for the first time.

The same goes for our Participant entity:

Participant participant = entityManager.find(Participant.class, 
    participantHolder.get().getId());

LOGGER.debug("Fetched participant");
assertEquals("ABC123", participant.getTicket().getRegistrationCode());

Hibernate generating the following SQL statements:

SELECT p.id AS id1_1_0_ ,
       p.event_id AS event_id3_1_0_
FROM   participant p
WHERE  p.id = 1

-- Fetched participant

SELECT p.ticket AS ticket2_1_
FROM   participant p
WHERE  p.id = 1

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Seize the deal! 40% discount. Seize the deal! 40% discount.

Conclusion

Fetching entity properties lazily comes in handy whenever we deal with BLOB, CLOB, BINARY, TEXT, and JSON column types. By default, all lazy properties are initialized together, but this behavior can be customized using @LazyGroup, as it will be explained in a future post.

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.