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.

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 the TABLE 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?

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

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.