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!

You can earn a significant passive income stream from promoting my book, courses, tools, training, or coaching subscriptions.

If you're interested in supplementing your income, then join my affiliate program.

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:

Contract Root Aggregate

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 a short type, as opposed to an int or a long, is not arbitrary. For more details about why using a short 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.

And there is more!

You can earn a significant passive income stream from promoting all these amazing products that I have been creating.

If you're interested in supplementing your income, then join my affiliate program.

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.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.