Java application performance tuning using Lightrun

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 show you analyze a Java application using Lightrun so that you can discover various performance tuning improvements you could apply to your current Java application.

In this previous article, I explained what Lightrun is and how you can use it to inject dynamic logs, capture runtime snapshots, or add dynamic metrics.

In this article, I’m going to use Lightrun as an alternative to my JPA Association Fetching Validator.

DefaultLoadEventListener

When fetching a JPA entity using Hibernate, a LoadEvent is triggered, which is handled by the DefaultLoadEventListener, as follows:

Fetching Entity First-Level Cache

The DefaultLoadEventListener will check whether the entity is located in the current JPA Persistence Context or first-level cache. If the entity is found there, then the very same Object reference is going to be returned.

This means that two consecutive entity fetch calls will always return the same Java Object reference. And this is the reason why JPA and Hibernate provide application-level repeatable reads.

If the entity is not found in the first-level cache, Hibernate will try to load it from the second-level cache if and only if the second-level cache was enabled.

Last, if the entity cannot be loaded from any cache, it will be loaded from the database.

Now, this process can happen when calling EntityManager.find, when traversing an association, or indirectly for the FetchType.EAGER strategy.

Inspecting N+1 query issues

The JPA Association Fetching Validator article explains how you can assert the JPA association fetches programmatically. This tool is very useful during testing, but it’s less practical for consultants who have to examine a production system for the very first time.

For instance, let’s take an example from the Spring PetClinic application:

@Entity
@Table(name = "pets")
public class Pet extends NamedEntity {

    @Column(name = "birth_date")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate birthDate;

    @ManyToOne
    @JoinColumn(name = "type_id")
    private PetType type;

    @ManyToOne
    @JoinColumn(name = "owner_id")
    private Owner owner;
    
}

The Pet entity has two parent associations, type and owner, each one being annotated with the @ManyToOne annotation. However, by default, the @ManyToOne association uses the FetchType.EAGER fetching strategy.

So, if we load 2 Pet entities while also fetching their associated owner associations:

List<Pet> pets = entityManager.createQuery("""
    select p
    from Pet p
    join fetch p.owner
    where p.id in :petIds
    """)
.setParameter("petIds", List.of(3L, 6L))
.getResultList();

Hibernate will execute 3 queries:

SELECT 
    p.id as id1_1_1_,
    p.name as name2_1_1_, 
    p.birth_date as birth_da3_1_1_, 
    p.owner_id as owner_id4_1_1_, 
    p.type_id as type_id5_1_1_, 
    o.id as id1_0_0_, 
    o.first_name as first_na2_0_0_, 
    o.last_name as last_nam3_0_0_, 
    o.address as address4_0_0_, 
    o.city as city5_0_0_, 
    o.telephone as telephon6_0_0_
FROM 
    pets p 
JOIN 
    owners o ON o.id = p.owner_id 
WHERE 
    p.id IN (3, 6)

SELECT 
    pt.id as id1_3_0_, 
    pt.name as name2_3_0_ 
FROM 
    types pt 
WHERE 
    pt.id = 3
    
SELECT 
    pt.id as id1_3_0_, 
    pt.name as name2_3_0_ 
FROM 
    types pt 
WHERE 
    pt.id = 6

So, why there were 3 queries executed instead of only 1? That’s the infamous N+1 query issue.

Java Performance Tuning using Lightrun

While you can detect the N+1 query issues using integration tests, sometimes you cannot do that because the system you were hired to analyze is deployed into production, and you haven’t seen the source code yet.

In this kind of situation, a tool like Lightrun becomes very handy as you can simply inject dynamically a runtime snapshot that’s recorded only when a given condition is met.

The first step is to add a runtime snapshot in the loadFromDatasource method of the DefaultLoadEventListener Hibernate class.

Lightrun DefaultLoadEventListener snapshot

Notice that the snapshot is recorded only of the isAssociationFetch() method of the associated LoadEvent returns true. This condition allows us to capture the secondary queries executed by the N+1 query issue.

Now, when loading all the pet owners with the last name of Davis, the PetClinic application executes the following SQL queries:

SELECT DISTINCT 
    o.id AS id1_0_0_,
    p.id AS id1_1_1_,
    o.first_name AS first_na2_0_0_,
    o.last_name AS last_nam3_0_0_,
    o.address AS address4_0_0_,
    o.city AS city5_0_0_,
    o.telephone AS telephon6_0_0_,
    p.name AS name2_1_1_,
    p.birth_date AS birth_da3_1_1_,
    p.owner_id AS owner_id4_1_1_,
    p.type_id AS type_id5_1_1_,
    p.owner_id AS owner_id4_1_0__,
    p.id AS id1_1_0__
FROM 
    owners o
LEFT OUTER JOIN 
    pets p ON o.id=p.owner_id
WHERE 
    o.last_name LIKE 'Davis%'
    
SELECT 
    pt.id as id1_3_0_, 
    pt.name as name2_3_0_ 
FROM 
    types pt 
WHERE 
    pt.id = 6

SELECT 
    pt.id as id1_3_0_, 
    pt.name as name2_3_0_ 
FROM 
    types pt 
WHERE 
    pt.id = 3

And when checking the Lightrun Snapshot console, we can see that two records have been registered:

The first snapshot looks as follows:

Lightrun DefaultLoadEventListener first result

And the second snapshot looks like this:

Lightrun DefaultLoadEventListener second result

Notice that the two snapshots correspond to the secondary queries executed by the Spring Petclinic application due to the extensive use of the FetchType.EAGER strategy.

Cool, right?

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

Conclusion

While you can detect these N+1 query issues during testing using the JPA Association Fetching Validator, if your task is to analyze a runtime system you’ve never ever seen before, then Lightrun is a great tool to discover all sorts of issues and the reason why they happen.

Especially because Java Performance Tuning is one of the most common reasons I’m getting hired, Lightrun is a great addition to my toolset.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published.

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