Hypersistence Optimizer User Guide

Introduction

The User Guide explains how you can customize the default Hypersistence Optimizer behavior and describes all performance-related events that are triggered while analyzing your project.

Configuring Hypersistence Optimizer

By default, Hypersistence Optimizer requires little configuration. However, as you will see in this guide, you can change the way events are propagated so that you can filter, decorate, or provide a custom exception handler.

Enabling or disabling scanners

Hypersistence Optimizer uses a mapping, a configuration, and a runtime scanner. The mapping and configuration scanners will trigger events when you create a HypersistenceOptimizer instance.

You can enable or disable any of the Hypersistence Optimizer scanners via the following methods:

  • setMappingScannerEnabled
  • setConfigurationScannerEnabled
  • setRuntimeScannerEnabled

There’s also a setMetadataScannersEnabled method that simply calls the setMappingScannerEnabled and setConfigurationScannerEnabled methods with the given parameter value.

So, if you want to disable the mapping and configuration scanners because you are always recreating the HypersistenceOptimizer instance in every integration test, then you can use the setMetadataScannersEnabled method of the associated JpaConfig or HibernateConfig objects.

HypersistenceOptimizer hypersistenceOptimizer = new HypersistenceOptimizer(
    new JpaConfig(entityManagerFactory)
        .setMetadataScannersEnabled(false)
);

EventHandler

The EventHandler allows you to customize the way events are processed by Hypersistence Optimizer. For instance, by default, all events are logged by the LogEventHandler instance.

Collecting events

By default, all events are collected automatically via an internal ListEventListener, and you can access the recorded events like this:

List<Event> events = hypersistenceOptimizer.getEvents();

By default, only 25k events are collected, but you can configure this threshold via the hypersistence.max_event_size configuration property. After the max_event_size threshold is reached, the oldest entries are going to be removed to ensure the event list doesn’t grow indefinitely.

If you are using Spring Boot, you can change the default value of this property in the application.properties file:

spring.jpa.properties.hypersistence.max_event_size=10000

If you are bootstrapping JPA using the persistence.xml configuration file, then you can set
the hypersistence.max_event_size configuration property like this:

<properties>
    ...
    <property name="hypersistence.max_event_size" value="10000"/>
</properties>

You can also set this property programmatically using the Config object when instantiating a new Hypersistence Optimizer instance:

new HypersistenceOptimizer(
    new JpaConfig(entityManagerFactory())
        .setProperties(
            Map.of(
                Config.Property.MAX_EVENT_SIZE, 10000
            )
        )
);

Adding a new EventHandler

If you want to add a new event handler, you can use the addEventHandler method:

List<String> tipsUrls = new ArrayList<>();

new HypersistenceOptimizer(
    new JpaConfig(entityManagerFactory())
        .addEventHandler(
            event -> {
                tipsUrls.add(event.getInfoUrl());
            }
        )
);

assertEquals(1, tipsUrls.size());

If you want to test this functionality, check out the AddEventHandlerTest in the hypersistence-optimizer GitHub repository.

EventFilter

The EventFilter allows you to accept or reject certain events that your not interested in capturing and propagating. For instance, if you’re using MySQL, using the IDENTITY identifier generator is the only reasonable option.

However, you might want to reject the IdentityGeneratorEvent that is associated with the IDENTITY generator, therefore preventing it from propagating further.

To do so, you need to provide a custom EventFilter as in the following example:

new HypersistenceOptimizer(
    new JpaConfig(entityManagerFactory())
        .setEventFilter(
            event -> !(event instanceof IdentityGeneratorEvent)
        )
);

Notice the setEventFilter method call that passes a custom EventFilter to the current Hypersistence Optimizer configuration.

If you want to test this functionality, check out the EventFilterTest in the hypersistence-optimizer GitHub repository.

How to trigger a build failure when detecting any performance issue

Now, one of the best things that Hypersistence Optimizer gives you is the ability to automatically detect JPA and Hibernate performance issues.

While you can always review the report generated by Hypersistence Optimizer, it’s even better if, after you fixed all issues, any data access code change that could introduce a performance-related issue be detected prior to pushing changes to production.

All you need to do is assert that no events are generated, as illustrated by the following code snippet:

@Test
public void testNoPerformanceIssues() {
    HypersistenceOptimizer hypersistenceOptimizer = new HypersistenceOptimizer(
        new JpaConfig(entityManagerFactory())
    );
    assertTrue(hypersistenceOptimizer.getEvents().isEmpty());
}

That’s it!

If any event is generated by Hypersistence Optimizer, the build will fail, and you’ll get instant notification of the performance-related issues that were introduced by the latest code changes.

Runtime configuration properties

Hypersistence Optimizer offers a flexible mechanism for providing runtime configurations, on a per-EntityManagerFactory, EntityManager, or Query basis.

Any mechanism that allows you to configure JPA and Hibernate properties can also be used to set the Hypersistence Config properties.

Configuring the runtime properties using the persistence.xml file

If you are bootstrapping JPA using the persistence.xml configuration file, then you can provide
the Hypersistence Optimizer runtime properties via the properties XML tag:

<properties>
    ...
    <property name="hypersistence.session.timeout_millis" value="250"/>
    <property name="hypersistence.session.flush_timeout_millis" value="100"/>
    <property name="hypersistence.query.max_result_size" value="50"/>
</properties>

Configuring the runtime properties using the application.properties file

If you are using Spring Boot, you can set the Hypersistence runtime properties just like you do with the Hibernate configuration properties.

Therefore, if you add the following entry in the application.properties file:

spring.jpa.properties.hypersistence.session.timeout_millis=250
spring.jpa.properties.hypersistence.session.flush_timeout_millis=100
spring.jpa.properties.hypersistence.query.max_result_size=50

Hypersistence Optimizer is going to trigger a runtime event in the following situations:

  • The JPA EntityManager or Hibernate Session runs for more than 250 milliseconds
  • The JPA EntityManager or Hibernate Session flush runs for more than 100 milliseconds
  • A Query returns a List with more than 50 elements.

Configuring the runtime properties when instantiating Hypersistence Optimizer

You can set the Hypersistence runtime properties programmatically using the Config object when instantiating a new Hypersistence Optimizer instance:

new HypersistenceOptimizer(
    new JpaConfig(entityManagerFactory())
        .setProperties(
            Map.of(
                Config.Property.Session.TIMEOUT_MILLIS, 250,
                Config.Property.Session.FLUSH_TIMEOUT_MILLIS, 100,
                Config.Property.Query.MAX_RESULT_SIZE, 50
            )
        )
);

The HypersistenceOptimizer programmatically-configured properties take precedence over the ones passed via JPA or Hibernate properties.

Configuring the runtime properties using the JPA EntityManager

If you’re using Hibernate ORM 5.2.8 or newer, you can also use the JPA EntityManager setProperty method to override a default Hypersistence property value, as well as properties defined via the EntityManagerFactory or HypersistenceOptimizer configuration:

entityManager.setProperty(
    Config.Property.Session.TIMEOUT_MILLIS,
    250
);

entityManager.setProperty(
    Config.Property.Session.FLUSH_TIMEOUT_MILLIS,
    100
);

entityManager.setProperty(
    Config.Property.Query.MAX_RESULT_SIZE,
    50
);

Configuring the runtime Query properties using the JPA Query hints

If you’re using Hibernate ORM 5.2 or newer, you can also use the JPA Query setHint method to override the default Hypersistence query property value, as well as the ones defined via the EntityManagerFactory, SessionFactory, EntityManager or HypersistenceOptimizer configuration:

List<Post> posts = entityManager.createQuery("""
    select p 
    from Post p 
    where p.createdOn >= :today
    order by p.id
    """, Post.class)
.setParameter(
    "today",
    Timestamp.valueOf(
        LocalDate.now().atStartOfDay()
    )
)
.setHint(Config.Property.Query.MAX_RESULT_SIZE, 50)
.getResultList();

Configuring the runtime Query properties using the Hibernate Query comment

You can also use the Hibernate Query setComment method to override the default Hypersistence query property value, as well as the ones defined via the EntityManagerFactory, SessionFactory, EntityManager or HypersistenceOptimizer configuration:

List<Post> posts = entityManager.createQuery("""
    select p 
    from Post p 
    where p.createdOn >= :today
    order by p.id
    """, Post.class)
.setParameter(
    "today",
    Timestamp.valueOf(
        LocalDate.now().atStartOfDay()
    )
)
.unwrap(org.hibernate.query.Query.class)
.setComment(
    new Config.Property.Stringifier()
        .put(Config.Property.Query.MAX_RESULT_SIZE, 10)
        .toString()
)
.getResultList();

Notice that we used the Config.Property.Stringifier utility, supplied by Hypersistence Optimizer, to transform the Map-based properties to their String representation that can be passed to the current Hibernate Query via the comment mechanism, and further used by Hypersistence Optimizer to override the Query runtime properties.

JMX

Hypersistence Optimizer provides support for JMX. By default, the JMX integration is disabled, and, to enable it, you need to set the JMX configuration, like this:

new HypersistenceOptimizer(
    new JpaConfig(entityManagerFactory())
        .setJmx(
            new Config.Jmx("Billing")
        )
);

The Config.Jmx object requires a unique Persistence Unit name, which is going to be used to export the MBean that gives you access to the generated events and the runtime properties.

Getting the events using JMX

Once you enabled the JMX integration, you can get access to the generated events, as illustrated by the following screen capture:

Hypersistence Optimizer - Getting the list of Events using the JMX bean

Getting and setting the runtime properties using JMX

You can also read or write the runtime properties (e.g., hypersistence.query.max_result_size) using the getProperty or setProperty methods of the JMX bean, as illustrated by the following screen capture:

Hypersistence Optimizer - Setting a property on the JMX bean

When Hypersistence Optimizer bootstraps, a banner is printed in the application log.

If you don’t want to disable the banner printing, then you can set the bannerPrintingEnabled property of the JpaConfig or HibernateConfig objects to the value of false, like this:

new HypersistenceOptimizer(
    new JpaConfig(entityManagerFactory())
        .setBannerPrintingEnabled(false)
);

Events

Hypersistence Optimizer generates events while scanning the application metadata.

Mapping events

The mapping events are related to JPA and Hibernate mappings, and Hypersistence Optimizer can detect the following mapping issues.

Identifier mapping

HiLoOptimizerEvent

The hi/lo algorithm, while it may reduce the number of database roundtrips required to assign the sequence-based identifiers, it does not interoperate well with other systems that are unaware of the underlying hi/lo strategy being used.

For this reason, you are better off using the pooled or pooled-lo optimizer generators.

IdentityGeneratorEvent

The IdentityGeneratorEvent is triggered when an entity identifier is mapped using the IDENTITY strategy. Unlike the SEQUENCE strategy, which can delay the entity INSERT statement until the Persistence Context is flushed, the IDENTITY strategy needs to execute the INSERT statement upon calling persist. For this reason, Hibernate cannot batch inserts for entities using the IDENTITY strategy.

Therefore, if the underlying database supports sequence objects, it is much more efficient to use the SEQUENCE strategy.

For more details about this topic, check out this article.

PostInsertGeneratorEvent

The PostInsertGeneratorEvent is triggered when an entity identifier is mapped using a post-insert strategy, like the SELECT strategy which uses a secondary SELECT statement to fetch the entity identifier generated by the database. Therefore, the only way Hibernate can get the entity identifier is if the associated row is inserted upon calling persist.

For this reason, the post-insert identifier strategies prevent Hibernate from batching inserts automatically.

For more details about this topic, check out this article.

PrimitiveIdentifierEvent

The PrimitiveIdentifierEvent instructs you that the entity identifier is mapped as a Java primitive (e.g. int, long) instead of a Java wrapper type (e.g. Integer or Long). The wrapper type allows nullable values, hence it works better with assigned entity identifiers since Hibernate knows that an entity with a null identifier does not have an associated table record.

TableGeneratorEvent

The TABLE identifier generator is very inefficient. Not only that it requires a separate database transaction as well as a separate database connection to make sure that the identifier generation process is not linked to the calling transaction, but it employs the use of row-level locks which are heavy compared to the lightweight latches used by the IDENTITY or SEQUENCE database identifier generation strategies.

The TABLE generator does not even scale well when increasing the number of threads that generate entity identifiers concurrently since it puts pressure on the underlying database connection pool, and it introduces a bottleneck at the identifier table level. You should use a SEQUENCE strategy if the database supports this feature, or use an IDENTITY column instead.

For more details about why you should avoid the TABLE generator, check out this article.

Basic mapping

EnumTypeStringEvent

The Java Enum can be mapped in two ways with JPA and Hibernate. By default, the Enum ordinal is used to materialize the Enum value in the database. However, to make it more readable, some developers choose to store the Enum name instead, which has a higher memory and disk footprint.

For more details about this topic, check out this article.

LargeColumnEvent

Table columns should be as compact as possible. For this reason, you should always strive for using the most compact types which can accommodate all possible values stored in a given column.

Unless you can choose more compact types, you might consider moving the large columns to a separate table. You could use a one-to-one table relationship to store the large columns, especially if they are not used frequently. Also, you could use multiple entities mapped to the same database table so that you can choose which properties are to be loaded from the database based on the entity type that you are currently fetching.

In case you have annotated the large column with @Basic(fetch=LAZY), you need to also activate the bytecode enhancement lazy loading mechanism as, otherwise, the column is going to be fetched eagerly when the entity is loaded.

If you cannot enable the bytecode enhancement lazy loading mechanism, you should at least use the @DynamicUpdate annotation on the entity level so that the UPDATE statement contains only the columns that have been modified by the currently running Persistence Context. This will reduce the impact of large columns on the underlying transaction log. Otherwise, not only that the transaction log gets bigger, but this can also affect replication since the log entries are propagated to follower nodes.

Version mapping

IntegerVersionColumnSizeEvent

The version property used for optimistic locking should be as compact as possible since its goal is to make sure that the underlying table row version matches the current entity version property. Since an integer column type takes 4 bytes, this could be a waste of space if the entity is not changed frequently.

A version property mapped to a smallint is sufficient for the vast majority of use cases since it allows the version to change 65000 times in between the read and the write of a given database table record.

For more details about this topic, check out this article.

LongVersionColumnSizeEvent

The version property used for optimistic locking should be as compact as possible since its goal is to make sure that the underlying table row version matches the current entity version property. Since a long or bigint column takes 8 bytes, this could be a waste of space if the entity is not changed frequently.

A version property mapped to a smallint is sufficient for the vast majority of use cases since it allows the version to change 65000 times in between the read and the write of a given database table record.

For more details about this topic, check out this article.

TimestampVersionEvent

When implementing a Concurrency Control algorithm, it is not a good idea to rely on physical or walk clock time because it is not reliable. Due to the NTP protocol, the clock time can jump backward, hence compromising the monotonic time reading property required by a concurrency control mechanism.

Therefore, you should favor using logical clocks instead, like the optimistic locking numerical-based version property.

For more details about this topic, check out this article.

Equality mapping

EqualsHashCodeEvent

The JPA entity could be stored in a Java Collection, like ArrayList or HashSet, or
as a key or value in a Java Map. For this reason, it’s very important to implement the Java equals and hashCode methods according to the Java equality contract, which defines the equivalence relation properties:

  • Reflexivity
  • Symmetry
  • Transitivity
  • Consistency

While the first three are quite easy to address, Consistency is trickier because, in the context of JPA, you need to make sure that multiple invocations of equals and hashCode yield the same result no matter the JPA entity state change (New, Managed, Detached, Removed).

For more details about this topic, check out this article.

Relationship mapping

BidirectionalSynchronizationEvent

When mapping a bidirectional one-to-many or many-to-many associations, you need to synchronize both sides of the association. Without synchronizing both sides, Hibernate cannot guarantee that the association state changes propagate to the database.

More, unsynchronized bidirectional associations can also lead to very subtle bugs that are hard to reproduce. For this reason, it’s best to have two utility methods (e.g., addEntity and removeEntity) on the mappedBy side of the one-to-many or many-to-many associations.

You should avoid the enableAssociationManagement Bytecode Enhancement option. Even if it aims to synchronize automatically both ends of a bidirectional association, this behavior works only from the parent-side to the child-side. The enableAssociationManagement option does not synchronize the @OneToMany side when the @ManyToOne association state changes.

For more details about this topic, check out this article.

ElementCollectionArrayEvent

The JPA element-collection mapped as a Java array is treated like a bag, meaning that elements are removed and reinserted back whenever the array is changed. For this reason, you are better off using a java.util.Set, which generates much more efficient SQL statements.

For more details about this topic, check out this article.

ElementCollectionEvent

The JPA element-collection requires all child operations to be executed from the parent side, hence, if you want to add or remove a child element, you always need to fetch the collection. For this reason, you should consider using a bidirectional one-to-many association instead or just use a @ManyToOne association and replace the collection with a query.

For more details about this topic, check out this article.

ElementCollectionListEvent

The JPA element-collection mapped as a java.util.List is treated like a bag, meaning that elements are removed and reinserted back whenever an element is removed from the List. For this reason, you are better off using a java.util.Set, which generates much more efficient SQL statements.

For more details about this topic, check out this article.

ManyToManyCascadeRemoveEvent

When mapping a many-to-many association, the CascadeType.REMOVE, which can also be inherited from CascadeType.ALL will propagate the remove entity state transition to the other entity, which is a parent, not a child entity.

Therefore, for many-to-many associations, we are only interested in removing the associated link table records, which is done automatically when dereferencing a child from the many-to-many collection.

For more details about this topic, check out this article.

ManyToManyListEvent

When mapping a many-to-many association, you should use a Java Set, not a List because the List is treated like a bag in Hibernate terminology, and a bag may generate inefficient SQL statements.

For more details about this topic, check out this article.

NullCollectionEvent

To avoid unnecessary null checks or, even worse, getting a NullPointerException, it’s better if all collections are initialized when the parent entity object is instantiated.

Hibernate uses collection proxies to enable the runtime lazy loading mechanism, so a collection is not null even prior to fetching the lazy collection.

OneToOneParentSideEvent

When mapping a one-to-one relationship, you can choose either a unidirectional or a bidirectional association. The unidirectional association will map the parent entity on the child side only, while the bidirectional association will also map the child entity reference in the parent entity.

Because at load time, Hibernate has no idea whether to assign null or a Proxy for the child entity reference on the parent side, a secondary query will be executed when fetching the parent entity without join fetching the child entity association. This can lead to N+1 query issues, which can, in turn, hurt application performance.

To fix this issue, you have two options. Either you use a unidirectional one-to-one association or use bytecode enhancement with the @LazyToOne(LazyToOneOption.NO_PROXY) to load the parent-side entity association lazily.

For more details about this topic, check out this article.

OneToOneWithoutMapsIdEvent

The one-to-one table relationship should be mapped so that the child table Primary Key is also a Foreign Key to the parent table Primary Key. This means that the parent and the child table records share their Primary Key values.

Without using @MapsId, the one-to-one association looks more like a one-to-many table relationship where the separate Foreign Key column has a unique key constraint. For this reason, you should always favor using @MapsId when mapping a one-to-one relationship.

For more details about this topic, check out this article.

SqlServerForeignKeyLockingEvent

By default, Hibernate includes all the non-identifier entity columns in the SET clause of an UPDATE statement, and, for this reason, on SQL Server, a shared lock is acquired on the parent table record to ensure that the Foreign Key is not going to reference a non-existing parent row.

To reduce the likelihood of locking and deadlocks, you are better off setting @DynamicUpdate on the entities containing @ManyToOne or non-shared @OneToOne association.

For more details about this topic, check out this article.

UnidirectionalOneToManyEvent

The unidirectional one-to-many association uses a link table and an additional Foreign Key column, which is not very efficient from a mapping perspective. Even worse, the generated SQL statements are not very efficient either, especially when using a Java List. You should consider using a bidirectional one-to-many association instead.

For more details about this topic, check out this article.

UnidirectionalOneToManyJoinTableEvent

The unidirectional one-to-many association that uses an explicit join column does not render very efficient SQL statements since the child entity insert is executed first while the collection is processed later at the Persistence Context flush time. For this reason, instead of a single INSERT, you’d see an INSERT and an UPDATE statement being executed. You should consider using a bidirectional one-to-many association instead.

For more details about this topic, check out this article.

Fetch strategy mapping

BatchFetchingEvent

Using batch fetching to initialize an association so to avoid the N+1 query issue is not very efficient and might indicate that the underlying collection is too large to be mapped as an entity association and that it should be replaced with an entity query instead.

EagerFetchingEvent

Using the FetchType.EAGER strategy is a very bad idea because, at mapping time, you have no indication of what might be needed when fetching an entity. The FetchType.EAGER strategy can also lead to N+1 query issues if the FetchType.EAGER association is not fetched in every JPQL or Criteria API query involving the entity defining this association.

For this reason, you should prefer using FetchType.LAZY instead, and override the lazy loading strategy at query time if that’s needed by the current business use case.

For more details about this topic, check out this article.

ExtraLazyCollectionEvent

The use of the LazyCollectionOption.EXTRA strategy indicates that the underlying collection is too large to be mapped as an entity association and that it should be replaced with an entity query instead.

For more details about this topic, check out this article.

SubSelectFetchingEvent

Using subselect fetching to initialize an association so to avoid the N+1 query issue might indicate that the underlying collection is too large to be mapped as an entity association and that it should be replaced with an entity query instead.

Also, you should consider using DTO projections, which can also be fetched as graphs, if you don’t plan to modify the entities being fetched.

Cache mapping

NaturalIdCacheEvent

The natural id mapping allows you to fetch an entity by its associated business key. However, this is a two-stage process. First, the entity identifier is resolved by its associated natural id. Second, with the entity identifier resolved, the entity is fetched from the Persistence Context.

To save the first query, you can use the NaturalIdCache entity mapping.

For more details about this topic, check out this article.

Inheritance mapping

StringDiscriminatorTypeEvent

The entity identifier discriminator column can be mapped as a numeric value, a char, or a String. The String strategy, although more readable, is the least compact strategy. For this reason, you should favor the numeric-based strategy, which can also be designed to be readable via a separate description table.

For more details about this topic, check out this article.

TablePerClassInheritanceEvent

The TABLE_PER_CLASS inheritance strategy is not very efficient, especially for polymorphic queries. You should use either SINGLE_TABLE or JOINED instead if you need to materialize the entity inheritance tree in the database. Otherwise, use @MappedSuperclass if the entity inheritance is only needed on the domain model side.

Configuration events

The configuration events are related to Hibernate, and Hypersistence Optimizer can detect the following configuration issues.

Batching configuration

JdbcBatchToBulkInsertEvent

Some databases, like PostgreSQL and MySQL, allow batch inserts to be executed as bulk multi-value INSERT statements.

This way, not only are network roundtrips saved, but the database engine has fewer statements to execute as well.

When using PostgreSQL, you can set the reWriteBatchedInserts property to the value of true to rewrite the JDBC batch insert as a bulk INSERT statement.

For more details about the PostgreSQL reWriteBatchedInserts JDBC Driver configuration property, check out this article.

When using MySQL, you can set the rewriteBatchedStatements property to the value of true, and the JDBC Driver will replace a JDBC batch insert with a bulk multi-value INSERT statement.

Check out this article for more details about the MySQL rewriteBatchedStatements JDBC Driver configuration property.

JdbcBatchOrderInsertsEvent

The hibernate.order_inserts configuration property should be enabled so that Hibernate can reorder SQL insert statements upon flushing the Persistence Context.

This is especially useful when cascading entity state transitions since, without reordering, the batch must be flushed when switching from one entity to another.

For more details about the hibernate.order_inserts configuration property, check out this article.

JdbcBatchOrderUpdatesEvent

The hibernate.order_updates configuration property should be enabled so that Hibernate can reorder SQL update statements upon flushing the Persistence Context.

This is especially useful when cascading entity state transitions since, without reordering, the batch must be flushed when switching from one entity to another.

For more details about the hibernate.order_updates configuration property, check out this article.

JdbcBatchSizeEvent

The hibernate.jdbc.batch_size configuration property should be set to an integer value greater than 1 (usually between 5 and 30) so that Hibernate can execute SQL statements in batches, therefore reducing the number of database network roundtrips.

For more details about the hibernate.jdbc.batch_size configuration property, check out this article.

JdbcBatchVersionedEntitiesEvent

You should consider enabling the hibernate.jdbc.batch_versioned_data configuration property so that Hibernate can batch updates and deletes for entities that have a @Version property. Otherwise, JDBC batch updates and deletes are disabled for the versioned entities.

This property was disabled prior to Hibernate 5, so make sure you enable it for Hibernate 3 and 4. If you’re using Oracle, you might want to upgrade the JDBC Driver from 10g to a newer version as well as upgrading the Hibernate Oracle Dialect to at least Oracle12cDialect to be able to batch update and delete statements for versioned entities.

For more details about the hibernate.jdbc.batch_versioned_data configuration property, check out this article.

Connection configuration

ConnectionReleaseAfterStatementEvent

When using JTA, Hibernate releases the associated database connection after every executing statement. As explained in this article, this strategy can incur a response time overhead. For this reason, if you are using a stand-alone JTA transaction manager (e.g. Bitronix, Atomikos) or a Java EE application server that does not require this aggressive connection release mode, then you should switch to releasing the connection when the transaction is ended.

Since Hibernate 5.2, you can set the connection acquisition and release strategy via the hibernate.connection.handling_mode configuration property. So, if you’re using JTA, you might want to provide the following configuration property, which instructs Hibernate to release the database connection only after the JPA transaction is ended.

<property 
    name="hibernate.connection.handling_mode" 
    value="delayed_acquisition_and_release_after_transaction"
/>

For Hibernate versions that are older than 5.2, you should use the hibernate.connection.release_mode configuration property instead. So, if you’re using JTA, you might want to provide the following configuration property, which instructs Hibernate to release the database connection only after the JPA transaction is ended.

<property 
    name="hibernate.connection.release_mode" 
    value="after_transaction"
/>
DriverManagerConnectionProviderEvent

The DriverManagerConnectionProvider is not suitable to be used in a production environment. Although it features a rudimentary connection pooling solution, you should consider using a professional pooling framework, like HikariCP.

DataSourceConnectionProviderEvent

The DataSourceConnectionProvider is the most flexible ConnectionProvider implementation as it allows you to chain as many DataSource proxies as you need, like connection pooling, logging, and monitoring.

Without using the DataSourceConnectionProvider, it’s hard to configure a tool like datasource-proxy which, not only that it gives you advanced logging capabilities, but you can use it to automatically detect N+1 query issues.

More, when using the DataSourceConnectionProvider, you can easily integrate FlexyPool which allows you to monitor the database connection usage and determine the right connection pool size.

SkipAutoCommitCheckEvent

As I explained in this article, when using RESOURCE_LOCAL transactions, which is the default for Spring and Spring Boot applications, the database connection is acquired eagerly at the beginning of the JPA transaction (when entering the @Transactional service method) because the auto-commit flag has to be checked and disabled accordingly.

If you already configured the underlying connection pool to disable the auto-commit flag when acquiring a JDBC Connection, then you should instruct Hibernate to skip the auto-commit check via the following configuration property:

<property
    name="hibernate.connection.provider_disables_autocommit"
    value="true"
/>

Dialect configuration

DialectVersionEvent

This event tells you that your application uses an older Hibernate Dialect, and you should switch to the latest Hibernate Dialect associated with the database server version your application is using.

Driver configuration

SqlServerSendStringParametersAsUnicodeEvent

This event tells you that your application has not disabled the sendStringParametersAsUnicode
SQL Server JDBC Driver setting.

By default, the SQL Server JDBC Driver sends all String bind parameter values in Unicode format, even when the underlying column type is CHAR, VARCHAR, or LONGVARCHAR.

In this case, if your SQL query uses a filter predicate on a CHAR, VARCHAR or LONGVARCHAR column that has an associated database index, the database will have to execute an expensive CONVERT_IMPLICIT action to transform the bind parameter value from the Unicode format to the original column format.

For this reason, the SQL Server JDBC Driver performance tuning page recommends disabling this default String format conversion behavior by setting the sendStringParametersAsUnicode JDBC Driver configuration property to the value of false.

For more details about the sendStringParametersAsUnicode SQL Server JDBC Driver setting, check out this article.

Envers configuration

DefaultAuditStrategyEvent

This event tells you that your application is using the Hibernate Envers DefaultAuditStrategy, which is not very efficient when it comes to generating the audit log queries. Therefore, it’s worth using the ValidityAuditStrategy instead. However, switching the audit log strategy requires changing the audit log tables, so take this into consideration prior to changing the strategy.

For more details about the Hibernate Envers ValidityAuditStrategy, check out this article.

Fetching configuration

JdbcFetchSizeEvent

For database systems which, by default, fetch the entire JDBC ResultSet in a single database roundtrip and cache it on the client-side (e.g. PostgreSQL, MySQL), there is no need to set the hibernate.jdbc.fetch_size configuration property.

However, if you’re using Oracle which, by default, uses a JDBC Statement fetch size of just 10, you might want to set the hibernate.jdbc.fetch_size configuration property to a value that’s greater than 10 as Hibernate could use fewer database roundtrips when iterating a JDBC ResultSet that contains multiple entities or DTO projections.

For more details about the hibernate.jdbc.fetch_size configuration property, check out this article.

TempSessionLazyLoadingEvent

You should not enable the hibernate.enable_lazy_load_no_trans configuration property as Hibernate will then open a temporary Session and database transaction to fetch each individual lazy association that was not initialized in the original and now closed Hibernate Session or JPA EntityManager.

Instead, you should always fetch all associations that you need to process in the context of a single database transaction and JPA Persistence Context.

For more details about the hibernate.enable_lazy_load_no_trans configuration property, check out this article.

Entity identifier configuration

PooledSequenceOptimizerEvent

You should enable the hibernate.id.new_generator_mappings configuration property so that Hibernate can use the pooled or pooled-lo identifier optimizers for sequence-based entity identifiers.

Note that the pooled and pooled-lo optimizers are not backward compatible with the legacy hilo optimizer that you might have used with Hibernate ORM 3 or 4. So, if you were using the hilo optimizer, then you would need to increase the current sequence number so that is greater than any previously allocated entity identifiers.

This property used to be disabled prior to Hibernate 5, so you might want to enable it for legacy Hibernate versions. For Hibernate 5 and later versions, you don’t need to set this property as it’s enabled by default.

Logging configuration

ConsoleStatementLoggingEvent

By enabling the hibernate.show_sql configuration property, SQL statements are going to be printed in the console, and that’s not a good idea.

You should remove this property and set the org.hibernate.SQL logger on the DEBUG level. Even better than Hibernate-based logging, a JDBC DataSource logging framework (e.g. datasource-proxy) is a much more flexible option.

For more details about the best way to log SQL statements, check out this article.

MultiLineStatementLoggingEvent

By enabling the hibernate.format_sql configuration property, Hibernate is going to format the SQL statement and log it on multiple lines. You should consider removing this property if you are using a log parser that does not support multiline parsing.

SqlCommentStatementLoggingEvent

By enabling the hibernate.use_sql_comments configuration property, Hibernate is going to append an SQL comment to the auto-generated statements. You should remove this property since, besides increasing the networking overhead, it can also affect the effectiveness of the statement caching mechanism.

Query configuration

CriteriaQueryLiteralHandlingModeEvent

You should set the hibernate.criteria.literal_handling_mode configuration property to the value of BIND, as Hibernate can then use bind parameter values instead of inlining the literal value. This strategy gives you a better chance to reuse an already generated SQL execution plan when varying the literal value.

For more details about the hibernate.criteria.literal_handling_mode configuration property, check out this article.

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 details about the hibernate.query.in_clause_parameter_padding configuration property, check out this article.

DefaultQueryPlanCacheMaxSizeEvent

The default maximum size of the Hibernate Query Plan Cache is 2048, and every query execution creates an entry in the Query Plan Cache.

The main advantage of the Query Plan Cache is that it allows us to avoid parsing JPQL, Criteria API, or collection filter queries on every execution. Once an entity query is compiled, Hibernate stores its associated AST (Abstract Syntax Tree) in the Query Plan Cache, so the next execution will get the AST from the cache, therefore skipping the entity query compilation.

However, SQL queries generate Query Plan Cache entries too in order to reuse the named parameter binding strategy. While the difference between a cache hit and a miss is not as significant for an SQL query, as it’s the case for entity queries, the SQL query cache entry is stored in the same Query Plan Cache.

So, the default Query Plan Cache size of 2048 might not be enough for a non-trivial application, and, for this reason, you should set the hibernate.query.plan_cache_max_size (or hibernate.query.plan_cache_max_soft_references in Hibernate 3.6) configuration property to a value that allows you to hold all JPQL, Criteria API, or SQL queries executed via JPA and Hibernate.

Starting with Hibernate 5.4, the Hibernate Statistics mechanism allows you to monitor the Query Plan Cache via the getQueryPlanCacheHitCount and getQueryPlanCacheMissCount methods.

For more details about the hibernate.query.plan_cache_max_size configuration property, check out this article.

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 details about the hibernate.query.fail_on_pagination_over_collection_fetch configuration property, check out this article.

Schema configuration

SchemaGenerationEvent

You should not use Hibernate to manage your database schema. Instead, you should use an incremental schema migration tool (e.g. Flyway, Liquibase) which allows you to use database-specific DDL constructs. By storing the migration scripts in the Version Control System along with the application source code, you will always remember why a certain schema change was done.

Statement caching configuration

JdbcStatementCacheSizeEvent

JDBC Statement caching allows you to reuse the statement metadata when executing the same statement over and over again. By using statement caching, you can reduce transaction response time and increase throughput.

The JDBC Drivers allow you to configure statement caching, so you should definitely activate this feature as well. Depending on the number of statements you are executing, you need to set the statement cache size accordingly.

A statement cache size of 256 is a good default value. Consider lowering the statement cache size if you don’t have enough memory and increasing it if you have plenty of memory and need to execute lots of distinct SQL statements.

For more details about why you should enable statement caching, check out this article.

Runtime events

The runtime events are generated when running a JPA EntityManager or Hibernate Session or when executing queries.

Session events

EntityAlreadyManagedEvent

Managed entities are automatically checked by Hibernate when the Persistence Context is flushed.

Calling merge, save, saveOrUpdate, or update for managed entities is redundant, and the operation should be removed as it only adds an extra overhead with no real benefit.

For more details about the redundant merge anti-pattern, check out this article.

FlushModeAutoEvent

The Hibernate Session uses the legacy FlushMode.AUTO strategy, that does not guarantee read-your-writes consistency for native SQL queries.

When using the default FlushMode.AUTO, Hibernate does not flush the Persistence Context prior to executing an SQL query, so the pending entity changes will not be visible to the query execution.

You should consider setting the flushMode property of the current running Session or Query to the value of ALWAYS].

Since Hibernate 5.2, you can also set the org.hibernate.flushMode Hibernate configuration property to the value of always, and all Session instances will use the FlushMode.ofALWAYSstrategy instead of the defaultFlushMode.AUTO`.

You can set the org.hibernate.flushMode Hibernate configuration property like this:

<property
    name="org.hibernate.flushMode"
    value="always"
/>
NPlusOneQueryEntityFetchingEvent

The NPlusOneQueryEntityFetchingEvent indicates that an entity has been fetched using multiple secondary SQL queries using secondary SQL queries instead of being fetched in one query using a JOIN FETCH clause.

N+1 query issues can cause significant performance issues because, even if each query is fast to execute, doing that tens or hundreds of times just adds up, and the more the data grows with time, the bigger the performance penalty will get.

That’s why a slow query log might not help identify N+1 query issues, and you’d eventually spot them using query execution statistics after the production system is already affected.

For more details about the N+1 query issue, check out this article.

OptimisticLockModeEvent

The OptimisticLockModeEvent indicates that a managed entity was locked using the javax.persistence.LockModeType#OPTIMISTIC mode without having the associated database record locked with the javax.persistence.LockModeType#PESSIMISTIC_READ or javax.persistence.LockModeType#PESSIMISTIC_WRITE strategy.

Without locking the underlying record, there’s a window of opportunity between the version check and the transition commit, in which time the entity could still be changed by a concurrent transaction, therefore defeating the purpose of this optimistic locking mechanism.

Ideally, the javax.persistence.LockModeType#PESSIMISTIC_READ or javax.persistence.LockModeType#PESSIMISTIC_WRITE should be acquired right before exiting the service method that triggers the transaction commit, like in the following example:

@Transactional
public PurchaseOrder orderProduct(Long productId, int quantity) {
    Product product = entityManager.find(
        Product.class,
        productId,
        LockModeType.OPTIMISTIC
    );
â €
    PurchaseOrder order = new PurchaseOrder()
        .setProduct(product)
        .setQuantity(quantity);
â €
    entityManager.persist(order);
â €
    entityManager.lock(product, LockModeType.PESSIMISTIC_READ);
â €
    return order;
}

For more details about the redundant merge anti-pattern, check out this article.

RedundantSessionFlushEvent

The RedundantSessionFlushEvent indicates that the current Persistence Context (JPA EntityManager or Hibernate Session) has been flushed without triggering any entity modification.

If a read-write transaction doesn’t change any entity, then you could switch to using a read-only Session that doesn’t need to pay the overhead of flushing entities.

For more details about read-only Session optimization, check out this article.

SecondaryQueryEntityFetchingEvent

The SecondaryQueryEntityFetchingEvent indicates that an entity has been fetched using a secondary SQL query instead of being loaded using a JOIN FETCH clause along with its parent entity.

This issue can happen with both FetchType.EAGER and FetchType.LAZY associations, and since secondary queries can easily turn into N+1 query issues, this anti-pattern should be avoided.

For more details about the N+1 query issue, check out this article.

SessionFlushTimeoutEvent

The SessionFlushTimeoutEvent indicates that the current Persistence Context (EntityManager or org.hibernate.Session) has been flushed for more than the maximum allowed time threshold, which is given by the hypersistence.session.flush_timeout_millis property value.

The more entities the current Persistence Context is managing, the longer it takes to process the dirty checking mechanism and synchronize the entity state changes with the database.

The flush execution time impacts the overall transaction response time, so you need to make sure that the current JPA EntityManager or Hibernate Session doesn’t contain a very large number of entities.

The default value is set to 1 second and can be overridden on a per-Persistence Context or Persistence Unit basis via the hypersistence.session.flush_timeout_millis runtime property.

Check out the runtime configuration properties section for more details about how you can customize this property value.

SessionTimeoutEvent

Running the org.hibernate.Session or javax.persistence.EntityManager for too long is to be avoided because it impacts user experience, consumes resources, and prevents other concurrent requests from using the acquired resources (e.g., database connections).

By default, the Persistence Context timeout value is set to 3 seconds. This value was chosen based on this report published by Google, which states that:

53% of visits are abandoned if a mobile site takes longer than three seconds to load.
— Google Data, Global, Mar. 2016.

The default value can be overridden on a per-Persistence Context or Persistence Unit basis.

Check out the runtime configuration properties section for more details about how you can customize this property value.

TableRowAlreadyManagedEvent

The TableRowAlreadyManagedEvent indicates that a given database table row is managed by an entity that was already bound to the current Persistence Context.

When having multiple entities mapped to the same database table, it’s important to load at most one entity that manages a given record.

Otherwise, you could end up having data consistency issues at flush time since multiple entity objects would synchronize the same table record time with different entity states.

TransactionlessSessionEvent

The TransactionlessSessionEvent indicates that the current Persistence Context is not running in a transactional context, and for this reason, it has acquired multiple JDBC Connection objects to execute various SQL queries.

By using a transactional org.hibernate.Session or javax.persistence.EntityManager, you can reuse the same JDBC Connection for all SQL statements that get executed.

Connection events

AutoCommittingConnectionEvent

The AutoCommittingConnectionEvent indicates that a JDBC java.sql.Connection was borrowed in auto-commit mode.

Borrowing a JDBC java.sql.Connection from the pool in auto-commit mode should be avoided as Hibernate needs to disable that mode and enforce explicit transaction boundaries.

StatementlessConnectionEvent

The StatementlessConnectionEvent indicates that a JDBC java.sql.Connection was borrowed without executing any SQL statement.

Borrowing a JDBC java.sql.Connection from the pool without executing any SQL statement should be avoided as it deprives other concurrent requests of using this connection.

TransactionReconnectionEvent

The TransactionReconnectionEvent indicates that a JDBC java.sql.Connection is borrowed multiple times during the lifetime of a given JPA or Hibernate transaction.

Ideally, you should borrow a JDBC java.sql.Connection from the pool only once when executing a JPA or Hibernate transaction.

Query events

MassQueryCacheInvalidationEvent

When the query cache is enabled, native SQL statements that don’t explicitly specify the table spaces they are affecting will end up invalidating the entire query cache.

For this reason, if you are using the query cache, then when executing a native SQL query that modifies data by calling the executeUpdate method on the Query object, then you need to make sure you define the affected tables via one of the following three methods:

PaginationWithoutOrderByEvent

When executing a JPQL, Criteria API, or native SQL query that uses pagination, you have to provide an ORDER BY clause so that the result set entries are sorted.

Without an ORDER BY clause, the result is not deterministic since SQL does not guarantee any particular ordering by default.

PassDistinctThroughEvent

In a JPA entity query, be it JPQL or Criteria API, the DISTINCT keyword has two meanings.

For scalar projection queries, the DISTINCT JPA entity query keyword needs to be passed to the underlying SQL query in order to remove duplicates when fetching the result set.

For example, when extracting the YEAR from a TIMESTAMP, using DISTINCT will allow us to remove duplicate YEARS associated with all timestamp values that belong to the same year.

However, if the entity query fetches parent entities along with their child associations, DISTINCT is used to deduplicate the Java entity object references that would, otherwise, be returned to the client. In this case, we don’t want the DISTINCT keyword to be passed to the underlying SQL query since there is no duplicate record in the result set (each child Primary Key is unique anyway).

So, for entity queries that use DISTINCT to remove duplicate Java entity references, you need to set the hibernate.query.passDistinctThrough JPA query hint to false.

For more details about this topic, check out this article.

QueryResultIteratorCountEvent

This event is triggered when the Session iterate method returns an Iterator that contains more elements than the maximum query result size, which is given by the hypersistence.query.max_result_size runtime property.

By default, the Query max result size is set to 100 elements. This value can be overridden on a per-query, Persistence Context, or Persistence Unit basis.

Check out the runtime configuration properties section for more details about how you can customize this property value.

QueryResultListSizeEvent

Fetching a very large List with a JPA or Hibernate Query should be avoided because it impacts both the user experience and consumes database and application resources.

By default, the Query maximum result size is set to 100 elements. This value can be overridden on a per-query, Persistence Context, or Persistence Unit basis.

Check out the runtime configuration properties section for more details about how you can customize this property value.

QueryTimeoutEvent

Running a query for too long is to be avoided because it impacts user experience, consumes resources, and prevents other concurrent requests from using the acquired resources (e.g., database connections).

By default, the query timeout value is set to 250 milliseconds. The default timeout value can be customized via the hypersistence.query.timeout_millis runtime property. Check out the runtime configuration properties section for more details about setting this property.

Release Notes

You can check out the release notes using the following link.