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.
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 11th of October about High-Performance SQL.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.
