Spring Boot performance tuning
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
While developing a Spring Boot application is rather easy, tuning the performance of a Spring Boot application is a more challenging task, as, not only it requires you to understand how the Spring framework works behind the scenes, but you have to know what is the best way to use the underlying data access framework, like Hibernate for instance.
In a previous article, I showed you how easily to optimize the performance of the Petclinic demo application. However, by default, the Petclinic Spring Boot application uses the in-memory HSQLDB database, which is not what you’d usually want your application to be optimized for.
In this article, we are going to switch the underlying database from the in-memory HSQLDB to MySQL and run Hypersistence Optimizer to generate a performance tuning report for the JPA and Hibernate data access layer in the context of the MySQL database.
Tuning the performance of a #SpringBoot app can be challenging. @vlad_mihalcea shows you #howto generate a performance tuning report for the #JPA and Hibernate data access layer in the context of the #MySQL database. https://t.co/V2CKQNcg4x pic.twitter.com/5Ii40fGXHo
— Java (@java) November 6, 2019
Spring Petclinic
Spring Petclinic is a demo application built using Spring Boot, which demonstrates the framework capabilities.
By default, Spring Boot uses HSQLDB, but while this in-memory database is used extensively for testing, in a production environment, you are more likely to use a database like MySQL or PostgreSQL.
Luckily, Spring Boot offers a MySQL configuration and a Spring profile, which we can use as a starting point for our analysis.
Switching tests to using the MySQL profile
First of all, we need to use the @ActiveProfiles
annotation to activate the mysql
Spring profile.
@RunWith(SpringRunner.class) @SpringBootTest @ActiveProfiles("mysql") public class PetclinicIntegrationTests { @Autowired private VetRepository vets; @Test public void testFindAll() throws Exception { vets.findAll(); vets.findAll(); // served from cache } }
After activating the mysql
Spring profile, Spring Boot is going to use the application-mysql.properties
configuration file to override settings from the default application.properties
settings file.
In our case, the only change that was needed in the application-mysql.properties
configuration file was to change the database connection credentials:
database=mysql spring.datasource.url=jdbc:mysql://localhost/petclinic spring.datasource.username=mysql spring.datasource.password=admin
Running Hypersistence Optimizer
As you can see in the PetclinicIntegrationTests
class, running Hypersitence Optimizer is very easy, as you just have to pass the EntityManagerFactory
instance to the HypersistenceOptimizer
object constructor, and call the init
method.
Hypersistence Optimizer : CRITICAL - TableGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.owner.Pet] entity uses the TABLE strategy, which does not scale very well. Consider using the IDENTITY identifier strategy instead, even if it does not allow JDBC batch inserts. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#TableGeneratorEvent Hypersistence Optimizer : CRITICAL - TableGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.owner.Owner] entity uses the TABLE strategy, which does not scale very well. Consider using the IDENTITY identifier strategy instead, even if it does not allow JDBC batch inserts. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#TableGeneratorEvent Hypersistence Optimizer : CRITICAL - TableGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.visit.Visit] entity uses the TABLE strategy, which does not scale very well. Consider using the IDENTITY identifier strategy instead, even if it does not allow JDBC batch inserts. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#TableGeneratorEvent Hypersistence Optimizer : CRITICAL - TableGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.owner.PetType] entity uses the TABLE strategy, which does not scale very well. Consider using the IDENTITY identifier strategy instead, even if it does not allow JDBC batch inserts. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#TableGeneratorEvent Hypersistence Optimizer : CRITICAL - TableGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.vet.Specialty] entity uses the TABLE strategy, which does not scale very well. Consider using the IDENTITY identifier strategy instead, even if it does not allow JDBC batch inserts. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#TableGeneratorEvent Hypersistence Optimizer : CRITICAL - TableGeneratorEvent - The [id] identifier attribute in the [org.springframework.samples.petclinic.vet.Vet] entity uses the TABLE strategy, which does not scale very well. Consider using the IDENTITY identifier strategy instead, even if it does not allow JDBC batch inserts. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#TableGeneratorEvent Hypersistence Optimizer : MAJOR - DialectVersionEvent - Your application is using the [org.hibernate.dialect.MySQL5Dialect] Hibernate-specific Dialect. Consider using the [org.hibernate.dialect.MySQL57Dialect] instead as it's closer to your current database server version [MySQL 8.0]. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#DialectVersionEvent
TableGeneratorEvent
The TableGeneratorEvent
is triggered because, in the previous article, we switched the entity identifier strategy from IDENTITY
to SEQUENCE
.
Using the SEQUENCE
identifier strategy is the best option if and only if the underlying database supports sequence objects. However, MySQL 8 does not support database sequences, and, for this reason, Hibernate switches to using the TABLE generator
, and, as explained in his article, the TABLE generator
is a terrible choice when it comes to application performance.
Switching to using IDENTITY for MySQL
So, in our case, we are better off using the IDENTITY
generator for MySQL. However, we don’t want to change the entity mappings to IDENTITY
as that we might want to deploy the Petclinic application on other database systems, such as Oracle, SQL Server, or PostgreSQL, which do support database sequences. So, we want the default mapping to use the SEQUENCE
strategy, but only change that to IDENTITY
for MySQL.
Even if the
IDENTITY
generator prevents Hibernate from batching INSERT statements at the JDBC level, it’s still better to use this strategy rather than theTABLE
generator. For more details about JDBC batch updates, check out this article.
And, the solution is actually very simple. In fact, I already explained it in this article.
By providing a MySQL-specific orm.xml
JPA configuration file that overrides the base class entity identifier strategy, we can switch to using IDENTITY
when using MySQL.
So, we are going to create an orm.xml
file that’s going to be deployed by the mysql
profile in the META-INF
folder in the application jar
file.
The orm.xml
configuration file looks as follows:
<?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd" version="2.2"> <mapped-superclass class="org.springframework.samples.petclinic.model.BaseEntity"> <attributes> <id name="id"> <generated-value strategy="IDENTITY"/> </id> </attributes> </mapped-superclass> </entity-mappings>
That’s it!
Now, when rerunning the PetclinicIntegrationTests
test case, Hypersistence Optimizer will generate the following report:
Hypersistence Optimizer : MINOR - 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. Since the database does not support the SEQUENCE identifier strategy, you need to use plain JDBC or some other data access framework to batch insert statements. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent Hypersistence Optimizer : MINOR - 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. Since the database does not support the SEQUENCE identifier strategy, you need to use plain JDBC or some other data access framework to batch insert statements. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent Hypersistence Optimizer : MINOR - 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. Since the database does not support the SEQUENCE identifier strategy, you need to use plain JDBC or some other data access framework to batch insert statements. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent Hypersistence Optimizer : MINOR - 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. Since the database does not support the SEQUENCE identifier strategy, you need to use plain JDBC or some other data access framework to batch insert statements. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent Hypersistence Optimizer : MINOR - 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. Since the database does not support the SEQUENCE identifier strategy, you need to use plain JDBC or some other data access framework to batch insert statements. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent Hypersistence Optimizer : MINOR - 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. Since the database does not support the SEQUENCE identifier strategy, you need to use plain JDBC or some other data access framework to batch insert statements. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#IdentityGeneratorEvent Hypersistence Optimizer : MAJOR - DialectVersionEvent - Your application is using the [org.hibernate.dialect.MySQL5Dialect] Hibernate-specific Dialect. Consider using the [org.hibernate.dialect.MySQL57Dialect] instead as it's closer to your current database server version [MySQL 8.0]. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#DialectVersionEvent
Notice that the TableGeneratorEvent
was replaced by the IdentityGenerator
. The IdentityGenerator
is generated because the IDENTITY
strategy prevents Hibernate from batching INSERT statements when calling persist
as it needs to know the entity identifier when associating the persisting entity with the first-level cache.
But since we don’t really have an option for the IdentityGenerator
when using MySQL, we can simply choose to ignore this event in our case.
Filtering Hyperstistence Optimizer events
Hypersitence Optimizer is very flexible. You can customize how the events are handled, whether you want them to be logged or collected to a List
, and you can choose to filter events as well.
To filter out the IdentityGeneratorEvent
, you need to configure the HyperstistenceOptimizer
bean like this:
@Configuration public class HypersistenceConfiguration { @Bean public HypersistenceOptimizer hypersistenceOptimizer( EntityManagerFactory entityManagerFactory) { return new HypersistenceOptimizer( new JpaConfig(entityManagerFactory) .setEventFilter( event -> !(event instanceof IdentityGeneratorEvent) ) ); } }
Now, when rerunning the PetclinicIntegrationTests
test case, Hypersistence Optimizer will prevent the IdentityGeneratorEvent
from being included in the report:
Hypersistence Optimizer : MAJOR - DialectVersionEvent - Your application is using the [org.hibernate.dialect.MySQL5Dialect] Hibernate-specific Dialect. Consider using the [org.hibernate.dialect.MySQL57Dialect] instead as it's closer to your current database server version [MySQL 8.0]. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#DialectVersionEvent
DialectVersionEvent
The DialectVersionEvent
is generated because the default configured Hibernate dialect is MySQL57Dialect
while the Petclinic application is running against MySQL 8.
So, we just have to include the MySQL8Dialect
in the Spring Boot application-mysql.properties
configuration file:
database=mysql spring.datasource.url=jdbc:mysql://localhost/petclinic spring.datasource.username=mysql spring.datasource.password=admin spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL8Dialect
And now, no issue is being reported by Hypersistence Optimizer. Cool, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
Hypersistence Optimizer is a very powerful tool that can help you detect JPA and Hibernate performance issues long before they hit the production system.
And, one of its greatest advantages is that it can do all these checks on every commit, so you will no longer overlook a JPA or Hibernate performance-related improvement because of a very tight development schedule.
All the optimizations presented in this article can be found in this GitHub repository.
