Spring Data Hibernate Entity Listeners
Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?
What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?
Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.
So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!
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
@Versionproperty uses ashorttype, as opposed to anintor along, is not arbitrary. For more details about why using ashortis 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_INCREMENTon 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_INCREMENTon 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.


