Spring Data Hibernate Entity Listeners
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
In this article, we are going to see how we can configure Spring Data to register several Hibernate Entity Listeners that can intercept entity state modifications.
As I explained in this article, JPA also offers an event listening mechanism that you can configure via the @EntityListeners
, @PostPersist
or @PostUpdate
, or PostRemove
annotations. However, the JPA solution is way too limited as it doesn’t allow you to interfere with the EntityManager
directly.
Therefore, I prefer using the Hibernate Entity Listeners mechanism, which you can easily integrate with Spring Data, as this article demonstrates.
Domain Model
For this article, we are going to use the following domain model:
The Contract
is the Root Aggregate, and all the other entities implement the RootAware
interface to indicate how they can access the root.
The Contract
entity has an optimistic locking version which we are going to use to avoid the Lost Update anomaly when signing a contract.
The reason why the
@Version
property uses ashort
type, as opposed to anint
or along
, is not arbitrary. For more details about why using ashort
is more practical, check out this article.
Aggregate conflict serialization
Now, what we want is to prevent Lost Updates across the entire contract aggregate. This means we want to catch any modification that happened with all the entities belonging to a Contract
between the time we read it and the time we have to propagate a modification to the database.
For the Contract
entity itself, it’s sufficient to have the @Version
property to intercept the entity modifications, but we are also interested in incrementing the version
property of the Contract
even for the changes that happened to the entities that belong to this root aggregate.
And, as I explained in this article, that could be accomplished with the awesome OPTIMISTIC_FORCE_INCREMENT
.
But, to intercept the child entity state changes, we need to use the Hibernate Event Listener mechanism so that we know when to apply the OPTIMISTIC_FORCE_INCREMENT
lock on the associated Contract
entity.
Intercepting INSERT statements using the Hibernate PersistEventListener
To intercept the creation of a child entity, we can use the following Hibernate PersistEventListener
:
public class RootAwareInsertEventListener implements PersistEventListener { private static final Logger LOGGER = LoggerFactory.getLogger( RootAwareInsertEventListener.class ); public static final RootAwareInsertEventListener INSTANCE = new RootAwareInsertEventListener(); @Override public void onPersist(PersistEvent event) throws HibernateException { final Object entity = event.getObject(); if (entity instanceof RootAware rootAware) { Object root = rootAware.root(); event.getSession().lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT); LOGGER.info( "Incrementing the [{}] entity version " + "because the [{}] child entity has been inserted", root, entity ); } } @Override public void onPersist(PersistEvent event, PersistContext persistContext) throws HibernateException { onPersist(event); } }
The RootAwareInsertEventListener
intercepts the creation of any entity, but we are only interested in the ones that implement the RootAware
interface, for which we are going to execute the following steps:
- we locate the associated root entity
- we apply the
LockModeType.OPTIMISTIC_FORCE_INCREMENT
on the root entity
Intercepting UPDATE and DELETE statements using the Hibernate FlushEntityEventListener
To intercept the updates and the deletes of any child entity, we can use the following Hibernate FlushEntityEventListener
:
public class RootAwareUpdateAndDeleteEventListener implements FlushEntityEventListener { private static final Logger LOGGER = LoggerFactory.getLogger( RootAwareUpdateAndDeleteEventListener.class ); public static final RootAwareUpdateAndDeleteEventListener INSTANCE = new RootAwareUpdateAndDeleteEventListener(); @Override public void onFlushEntity(FlushEntityEvent event) throws HibernateException { final EntityEntry entry = event.getEntityEntry(); final Object entity = event.getEntity(); final boolean mightBeDirty = entry.requiresDirtyCheck(entity); if (mightBeDirty && entity instanceof RootAware rootAware) { if (isEntityUpdated(event)) { Object root = rootAware.root(); LOGGER.info( "Incrementing the [{}] entity version " + "because the [{}] child entity has been updated", root, entity ); event.getSession().lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT); } else if (isEntityDeleted(event)) { Object root = rootAware.root(); LOGGER.info( "Incrementing the [{}] entity version " + "because the [{}] child entity has been deleted", root, entity ); event.getSession().lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT); } } } private boolean isEntityUpdated(FlushEntityEvent event) { final EntityEntry entry = event.getEntityEntry(); final Object entity = event.getEntity(); int[] dirtyProperties; EntityPersister persister = entry.getPersister(); final Object[] values = event.getPropertyValues(); SessionImplementor session = event.getSession(); if (event.hasDatabaseSnapshot()) { dirtyProperties = persister.findModified( event.getDatabaseSnapshot(), values, entity, session ); } else { dirtyProperties = persister.findDirty( values, entry.getLoadedState(), entity, session ); } return dirtyProperties != null; } private boolean isEntityDeleted(FlushEntityEvent event) { return event.getEntityEntry().getStatus() == Status.DELETED; } }
The RootAwareUpdateAndDeleteEventListener
intercepts the modification of any entity. After filtering the entities that implement the RootAware
interface, we are going to execute the following steps:
- we locate the associated root entity
- we apply the
LockModeType.OPTIMISTIC_FORCE_INCREMENT
on the root entity
Registering the Hibernate Event Listeners with Spring
To register the Event Listeners, we can define a custom Hibernate Integrator
implementation like this:
public class RootAwareEventListenerIntegrator implements Integrator { public static final RootAwareEventListenerIntegrator INSTANCE = new RootAwareEventListenerIntegrator(); @Override public void integrate( Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) { final EventListenerRegistry eventListenerRegistry = sessionFactory .getServiceRegistry() .getService(EventListenerRegistry.class); eventListenerRegistry.appendListeners( EventType.PERSIST, RootAwareInsertEventListener.INSTANCE ); eventListenerRegistry.appendListeners( EventType.FLUSH_ENTITY, RootAwareUpdateAndDeleteEventListener.INSTANCE ); } @Override public void disintegrate( SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { } }
But, we still have to make the RootAwareEventListenerIntegrator
available to Hibernate, and this can be done via the Java-based Spring bean configuration:
@Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); ... entityManagerFactoryBean.setJpaProperties(additionalProperties()); return entityManagerFactoryBean; } protected Properties additionalProperties() { Properties properties = new Properties(); properties.put( EntityManagerFactoryBuilderImpl.INTEGRATOR_PROVIDER, (IntegratorProvider) () -> List.of( RootAwareEventListenerIntegrator.INSTANCE ) ); return properties; }
That’s it!
The EntityManagerFactoryBuilderImpl.INTEGRATOR_PROVIDER
is the Hibernate setting that can be used to provide an IntegratorProvider
that defines what Integrator
instances are to be registered by Hibernate.
Testing time
Now, when creating a new Annex
child entity for a Contract
:
entityManager.persist( new Annex() .setId(3L) .setDetails("Spring 6 Migration Training") .setContract( entityManager.getReference( Contract.class, 1L ) ) );
Hibernate will generate the following SQL statements:
INSERT INTO annex ( contract_id, details, id ) VALUES ( 1, 'Spring 6 Migration Training', 3 ) UPDATE contract SET version=3 WHERE id=1 and version=2
Notice that the version of the associated Contract
was changed since we added a new Annex
associated with it.
And, if we remove any child entity:
entityManager.remove( entityManager.getReference(Annex.class, 3L) );
Hibernate will delete the entity record and increment the contract version:
DELETE FROM annex WHERE id=3 UPDATE contract SET version=4 WHERE id=1 and version=3
Awesome, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
Using the Hibernate Event Listeners with Spring is very easy.
All you have to do is register the Event Listeners via an Integrator
which can be passed to Hibernate via the hibernate.integrator_provider
configuration property.
