Tuning Spring Petclinic JPA and Hibernate configuration with Hypersistence Optimizer

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, we are going to see how we can tune the performance of the Spring Petclinic application using Hypersistence Optimizer.

Now, while you can manually analyze your data access layer to make sure that JPA and Hibernate are properly configured, it’s much better if you can automate this task. That’s because new entities might be mapped in the future, and you want to make sure that the same performance-specific rules are consistently applied on every commit.

Hypersistence Optimizer allows you to automatically detect JPA and Hibernate issues during development, so you can optimize your application long before you launch it into production.

Spring Petclinic with Hypersistence Optimizer

Setting up Hypersistence Optimizer with Spring Petclinic

The Spring Petclinic project is a fully-functional application demonstrating Spring capabilities. It’s similar to the Java Pet Store application that’s been available since J2EE times.

After forking the Spring Petclinic, we need to add the Hypersistence Optimizer dependency to the pom.xml Maven configuration file.

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-optimizer</artifactId>
    <version>${hypersistence-optimizer.version}</version>
</dependency>

The only thing we need to add is the HypersistenceOptimizer bean that takes a reference to the currently configured EntityManagerFcatory:

@Configuration
public class HypersistenceConfiguration {
    @Bean
    public HypersistenceOptimizer hypersistenceOptimizer(
            EntityManagerFactory entityManagerFactory) {
        return new HypersistenceOptimizer(
            new JpaConfig(
                entityManagerFactory
            )
        );
    }
}

That’s it!

Testing time

With Hypersistence Optimizer in place, it’s time to run the PetclinicIntegrationTests and check the application log which looks as follows:

Hypersistence Optimizer: CRITICAL - IdentityGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.owner.Pet] entity uses the [IdentityGenerator] strategy, which prevents Hibernate from enabling JDBC batch inserts. Consider using the SEQUENCE identifier strategy instead. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent

Hypersistence Optimizer: CRITICAL - EagerFetchingEvent - The [owner] attribute in the [org.springframework.samples.petclinic.owner.Pet] entity uses eager fetching. Consider using a lazy fetching which, not only that is more efficient, but it is way more flexible when it comes to fetching data. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#EagerFetchingEvent

Hypersistence Optimizer: CRITICAL - EagerFetchingEvent - The [type] attribute in the [org.springframework.samples.petclinic.owner.Pet] entity uses eager fetching. Consider using a lazy fetching which, not only that is more efficient, but it is way more flexible when it comes to fetching data. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#EagerFetchingEvent

Hypersistence Optimizer: CRITICAL - EagerFetchingEvent - The [visits] attribute in the [org.springframework.samples.petclinic.owner.Pet] entity uses eager fetching. Consider using a lazy fetching which, not only that is more efficient, but it is way more flexible when it comes to fetching data. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#EagerFetchingEvent

Hypersistence Optimizer: CRITICAL - BidirectionalSynchronizationEvent - The [visits] bidirectional association in the [org.springframework.samples.petclinic.owner.Pet] entity requires both ends to be synchronized. Only the [addVisit(org.springframework.samples.petclinic.visit.Visit visit)] could be found. Consider adding the [removeVisit(org.springframework.samples.petclinic.visit.Visit visit)]  synchronization method as well. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#BidirectionalSynchronizationEvent

Hypersistence Optimizer: CRITICAL - IdentityGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.owner.Owner] entity uses the [IdentityGenerator] strategy, which prevents Hibernate from enabling JDBC batch inserts. Consider using the SEQUENCE identifier strategy instead. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent

Hypersistence Optimizer: CRITICAL - BidirectionalSynchronizationEvent - The [pets] bidirectional association in the [org.springframework.samples.petclinic.owner.Owner] entity requires both ends to be synchronized. Only the [addPet(org.springframework.samples.petclinic.owner.Pet pet)] could be found. Consider adding the [removePet(org.springframework.samples.petclinic.owner.Pet pet)]  synchronization method as well. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#BidirectionalSynchronizationEvent

Hypersistence Optimizer: CRITICAL - IdentityGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.visit.Visit] entity uses the [IdentityGenerator] strategy, which prevents Hibernate from enabling JDBC batch inserts. Consider using the SEQUENCE identifier strategy instead. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent

Hypersistence Optimizer: CRITICAL - IdentityGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.owner.PetType] entity uses the [IdentityGenerator] strategy, which prevents Hibernate from enabling JDBC batch inserts. Consider using the SEQUENCE identifier strategy instead. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent

Hypersistence Optimizer: CRITICAL - IdentityGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.vet.Specialty] entity uses the [IdentityGenerator] strategy, which prevents Hibernate from enabling JDBC batch inserts. Consider using the SEQUENCE identifier strategy instead. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent

Hypersistence Optimizer: CRITICAL - IdentityGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.vet.Vet] entity uses the [IdentityGenerator] strategy, which prevents Hibernate from enabling JDBC batch inserts. Consider using the SEQUENCE identifier strategy instead. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent

Hypersistence Optimizer: CRITICAL - EagerFetchingEvent - The [specialties] attribute in the [org.springframework.samples.petclinic.vet.Vet] entity uses eager fetching. Consider using a lazy fetching which, not only that is more efficient, but it is way more flexible when it comes to fetching data. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#EagerFetchingEvent

Hypersistence Optimizer: MAJOR    - SkipAutoCommitCheckEvent - You should set the [hibernate.connection.provider_disables_autocommit] configuration property to [true] while also making sure that the underlying DataSource is configured to disable the auto-commit flag whenever a new Connection is being acquired. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#SkipAutoCommitCheckEvent

Hypersistence Optimizer: CRITICAL - JdbcBatchSizeEvent - If you set the [hibernate.jdbc.batch_size] configuration property to a value greater than 1 (usually between 5 and 30), Hibernate can then execute SQL statements in batches, therefore reducing the number of database network roundtrips. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#JdbcBatchSizeEvent

Hypersistence Optimizer: CRITICAL - QueryPaginationCollectionFetchingEvent - You should set the [hibernate.query.fail_on_pagination_over_collection_fetch] configuration property to the value of [true], as Hibernate can then prevent in-memory pagination when join fetching a child entity collection. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#QueryPaginationCollectionFetchingEvent

Hypersistence Optimizer: MAJOR    - QueryInClauseParameterPaddingEvent - You should set the [hibernate.query.in_clause_parameter_padding] configuration property to the value of [true], as Hibernate entity queries can then make better use of statement caching and fewer entity queries will have to be compiled while varying the number of parameters passed to the in query clause. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#QueryInClauseParameterPaddingEvent

Expaining the performance issues reported by Hypersistence Optimizer

A total of 14 issues have been reported which we will group by type and explain what we need to do to address them.

IdentityGeneratorEvent

The IdentityGeneratorEvent issue was reported 6 times.

The problem is that all entities (e.g. Pet, Owner, Visit, PetType, Specialty, Vet) inherit the entity identifier definition from BaseEntity who uses the IDENTITY startegy:

@MappedSuperclass
public class BaseEntity 
        implements Serializable {
    @Id
    @GeneratedValue(
        strategy = GenerationType.IDENTITY
    )
    private Integer id;

    //Getters and setters omitted for brevity
}

To fix the issue, you need to use GenerationType.SEQUENCE as Hibernate can then use batch inserts during the Persistence Context flush time.

The problem with the IDENTITY entity identifier strategy is that it prevents Hibernate from batching INSERT statements at flush time. For more details about this issue, check out the Hypersistence Optimizer documentation issue description.

EagerFetchingEvent

The EagerFetchingEvent tells you that a given entity association is fetched eagerly by Hibernate, and that can lead to N+1 query issues or make your application fetch more data than necessary.

In the Pet entity, the type and owner associations are implicitly fetched eagerly since, by default, @ManyToOne and @OneToOne associations use the FetchType.EAGER strategy.

What’s more concerning is that the visits collection is also set to FetchType.EAGER because it’s unlikely that every use case will ever need this association initialized, and currently, fetching a single Pet entity will require 3 additional joins.

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

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

@OneToMany(cascade = CascadeType.ALL, mappedBy = "petId", fetch = FetchType.EAGER)
private Set<Visit> visits = new LinkedHashSet<>();

Also, the Vet entity defines a @ManyToMany association that uses FetchType.EAGER:

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
    name = "vet_specialties", 
    joinColumns = @JoinColumn(
        name = "vet_id"
    ), 
    inverseJoinColumns = @JoinColumn(
        name = "specialty_id"
    )
)
private Set<Specialty> specialties;

To fix the issue, you need to use FetchType.LAZY for all associations. For more details about this issue, check out the Hypersistence Optimizer documentation issue description.

BidirectionalSynchronizationEvent

The BidirectionalSynchronizationEvent tells you that a bidirectional association doesn’t benefit from the addEntity and removeEntity synchronization methods. Without synchronizing both ends of a bidirectional association, there is no guarantee that association changes will be propagated to the database.

To fix the issue, you need to add the removePet method in the Owner entity:

public void removePet(Pet pet) {
    getPetsInternal().remove(pet);
    pet.setOwner(null);
}

as well as the removeVisit method in the Pet entity:

public void removeVisit(Visit visit) {
    getVisitsInternal().remove(visit);
    visit.setPetId(null);
}

For more details about this issue, check out the Hypersistence Optimizer documentation issue description.

SkipAutoCommitCheckEvent

The SkipAutoCommitCheckEvent tells you that you need to enable the hibernate.connection.provider_disables_autocommit Hibernate configuration property. This way, the auto-commit check done by Hibernate at the beginning of a RESOURCE_LOCAL transaction can be skipped as long as you already disabled the auto-commit at the JDBC connection pool level.

To fix the issue, you need to add the following configuration properties to the application.properties file:

spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
spring.datasource.hikari.auto-commit=false

For more details about this issue, check out the Hypersistence Optimizer documentation issue description.

JdbcBatchSizeEvent

The JdbcBatchSizeEvent tells you that you need to set up the hibernate.jdbc.batch_size Hibernate configuration property. This way, Hibernate can use JDBC batching at flush time.

To fix the issue, you need to add the following configuration properties to the application.properties file:

spring.jpa.properties.hibernate.jdbc.batch_size=5
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

For more details about this issue, check out the Hypersistence Optimizer documentation issue description.

QueryPaginationCollectionFetchingEvent

The QueryPaginationCollectionFetchingEvent tells you that you need to enable the hibernate.query.fail_on_pagination_over_collection_fetch Hibernate configuration property. This way, Hibernate will fail whenever in-memory pagination is being done instead of just logging a warning message that might be overlooked.

To fix the issue, you need to add the following configuration properties to the application.properties file:

spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true

For more details about this issue, check out the Hypersistence Optimizer documentation issue description.

QueryInClauseParameterPaddingEvent

The QueryInClauseParameterPaddingEvent tells you that you need to enable the hibernate.query.in_clause_parameter_padding Hibernate configuration property. This way, your application can better take advantage of statement caching as Hibernate will use fewer IN clause parameter combinations.

To fix the issue, you need to add the following configuration properties to the application.properties file:

spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true

For more details about this issue, check out the Hypersistence Optimizer documentation issue description.

To see all the changes that I’ve done to address the problems reported by Hypersitence Optimizer, check out this GitHub commit. Now, when the test runs, no issue is being reported.

Cool, right?

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

Hypersistence Optimizer is very useful if your application uses JPA and Hibernate.

Not only that it can detect JPA mapping issues, but it can also scan the Hibernate configurations and let you know what you need to change so that your application works faster.

And it can do that on every commit, so you will no longer overlook a JPA or Hibernate performance-related improvement.

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.