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 thesetMetadataScannersEnabled
method of the associatedJpaConfig
orHibernateConfig
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 themax_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 thehypersistence-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 thehypersistence-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 HibernateSession
runs for more than 250 milliseconds - The JPA
EntityManager
or HibernateSession
flush runs for more than 100 milliseconds - A
Query
returns aList
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 theMap
-based properties to theirString
representation that can be passed to the current HibernateQuery
via thecomment
mechanism, and further used by Hypersistence Optimizer to override theQuery
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:
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:
Banner
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. TheenableAssociationManagement
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.of
ALWAYSstrategy instead of the default
FlushMode.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.
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.
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.