How to intercept entity changes with Hibernate event 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 the Hibernate event listeners work and how you add your custom listeners to intercept entity changes and replicate them to other database tables.

Recently, one of my blog readers asked a very good question on StackOverflow.

Since my main goal as a Hibernate Developer Advocate is to help Java developers get the most out of JPA and Hibernate, I decided that this is a good opportunity to talk about the Hibernate event listener mechanism.

Domain Model

Let’s assume we want to migrate our application to use a new database table (e.g. post) instead of the old one (e.g. old_post). The post and old_post tables look as follows:

Hibernate event listener tables

Both database tables share the Primary Key, and the id column of the old_post table is both the Primary Key and a Foreign Key to the post table id column. This way, we can ensure that all records that exist in the old_post table, exist as well in the new table we want to migrate to.

We only need to map the Post entity, and changes to the newer entity are going to be replicated to the old_post table as well:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    @Column(name = "created_on")
    private LocalDate createdOn = LocalDate.now();

    @Version
    private int version;
}

The newer post table features a new column as well, which is going to be skipped when replicating changes done to the post table.

Replicating changes using CDC

There are many ways you can replicate changes that happen in a database system. This feature is called CDC (Change Data Capture).

The most popular CDC method is to use database triggers. A lesser-known method is to parse the database transaction log (e.g. Redo Log in Oracle, Write-Ahead Log in PostgreSQL) using a tool like Debezium.

If your application executes all database operations through Hibernate, you can also use the Hibernate event listener mechanism to intercept entity changes.

Hibernate event system

Behind the scenes, Hibernate uses an event-based system to handle entity state transitions. The org.hibernate.event.spi.EventType Java Enum defines all event types that are supported by Hibernate.

When you call the EntityManager persist method, Hibernate fires a PersistEvent that is handled by the DefaultPersistEventListener. You can either substitute the default event listeners using your own implementations of the associated event listener interfaces or you can append pre-event and post-event listeners like PreInsertEventListener or PostInsertEventListener to fire before or after an entity is inserted.

Intercepting the entity insert event

To intercept the entity insert event, we can use the following ReplicationInsertEventListener that implements the Hibernate PostInsertEventListener interface:

public class ReplicationInsertEventListener 
        implements PostInsertEventListener {

    public static final ReplicationInsertEventListener INSTANCE = 
        new ReplicationInsertEventListener();

    @Override
    public void onPostInsert(
            PostInsertEvent event) 
            throws HibernateException {
        final Object entity = event.getEntity();

        if(entity instanceof Post) {
            Post post = (Post) entity;

            event.getSession().createNativeQuery(
                "INSERT INTO old_post (id, title, version) " +
                "VALUES (:id, :title, :version)")
            .setParameter("id", post.getId())
            .setParameter("title", post.getTitle())
            .setParameter("version", post.getVersion())
            .setFlushMode(FlushMode.MANUAL)
            .executeUpdate();
        }
    }

    @Override
    public boolean requiresPostCommitHanding(
            EntityPersister persister) {
        return false;
    }
}

So, after a Post entity is inserted, we run an additional SQL INSERT statement to create a mirroring record in the old_post table.

Intercepting the entity update event

To intercept the entity update event, we can use the following ReplicationUpdateEventListener that implements the Hibernate PostUpdateEventListener interface:

public class ReplicationUpdateEventListener 
        implements PostUpdateEventListener {

    public static final ReplicationUpdateEventListener INSTANCE = 
        new ReplicationUpdateEventListener();

    @Override
    public void onPostUpdate(
            PostUpdateEvent event) {
        final Object entity = event.getEntity();

        if(entity instanceof Post) {
            Post post = (Post) entity;

            event.getSession().createNativeQuery(
                "UPDATE old_post " +
                "SET title = :title, version = :version " +
                "WHERE id = :id")
            .setParameter("id", post.getId())
            .setParameter("title", post.getTitle())
            .setParameter("version", post.getVersion())
            .setFlushMode(FlushMode.MANUAL)
            .executeUpdate();
        }
    }

    @Override
    public boolean requiresPostCommitHanding(
            EntityPersister persister) {
        return false;
    }
}

After a Post entity is updated, we execute an SQL UPDATE statement to change the mirroring record in the old_post table.

Intercepting the entity delete event

To intercept the entity delete event, we can use the following ReplicationDeleteEventListener that implements the Hibernate PreDeleteEventListener interface:

public class ReplicationDeleteEventListener 
        implements PreDeleteEventListener {

    public static final ReplicationDeleteEventListener INSTANCE = 
        new ReplicationDeleteEventListener();

    @Override
    public boolean onPreDelete(
            PreDeleteEvent event) {
        final Object entity = event.getEntity();

        if(entity instanceof Post) {
            Post post = (Post) entity;

            event.getSession().createNativeQuery(
                "DELETE FROM old_post " +
                "WHERE id = :id")
            .setParameter("id", post.getId())
            .setFlushMode(FlushMode.MANUAL)
            .executeUpdate();
        }

        return false;
    }
}

While for insert and update we used the post-insert and post-update event listeners, for the delete operation, we need to use the pre-delete event listener since the old_post record must be deleted prior to removing the parent post record.

Registering the custom entity listeners

To register the custom event listeners we have just created, we can implement the org.hibernate.integrator.spi.Integrator interface to append the listeners to the Hibernate EventListenerRegistry.

public class ReplicationEventListenerIntegrator 
        implements Integrator {

    public static final ReplicationEventListenerIntegrator INSTANCE = 
        new ReplicationEventListenerIntegrator();

    @Override
    public void integrate(
            Metadata metadata,
            SessionFactoryImplementor sessionFactory,
            SessionFactoryServiceRegistry serviceRegistry) {

        final EventListenerRegistry eventListenerRegistry =
                serviceRegistry.getService(EventListenerRegistry.class);

        eventListenerRegistry.appendListeners(
            EventType.POST_INSERT, 
            ReplicationInsertEventListener.INSTANCE
        );
        
        eventListenerRegistry.appendListeners(
            EventType.POST_UPDATE, 
            ReplicationUpdateEventListener.INSTANCE
        );
        
        eventListenerRegistry.appendListeners(
            EventType.PRE_DELETE, 
            ReplicationDeleteEventListener.INSTANCE
        );
    }

    @Override
    public void disintegrate(
            SessionFactoryImplementor sessionFactory,
            SessionFactoryServiceRegistry serviceRegistry) {

    }
}

To instruct Hibernate to use the ReplicationEventListenerIntegrator, we need to set up the hibernate.integrator_provider configuration property which must be set programmatically to take an IntegratorProvider object.

To see how you can set the hibernate.integrator_provider configuration property when using Spring with JPA or Spring with Hibernate, check out this article.

Testing time

Now, when persisting a Post entity:

Post post1 = new Post();
post1.setId(1L);
post1.setTitle(
    "The High-Performance Java Persistence book is to be released!"
);

entityManager.persist(post1);

Hibernate executes the following SQL insert statements:

INSERT INTO post (
    created_on, 
    title, 
    version, 
    id
)
VALUES (
    '2018-12-12',
    'The High-Performance Java Persistence book is to be released!',
    0,
    1
)
        
INSERT INTO old_post (
    id, 
    title, 
    version
) 
VALUES (
    1, 
    'The High-Performance Java Persistence book is to be released!', 
    0
)

Now, when updating the previously inserted Post entity and creating a new Post:

Post post1 = entityManager.find(Post.class, 1L);
post1.setTitle(
    post1.getTitle().replace("to be ", "")
);

Post post2 = new Post();
post2.setId(2L);
post2.setTitle(
    "The High-Performance Java Persistence book is awesome!"
);

entityManager.persist(post2);

Hibernate executes the following SQL statements:

SELECT 
    p.id as id1_1_0_, 
    p.created_on as created_2_1_0_, 
    p.title as title3_1_0_, 
    p.version as version4_1_0_ 
FROM 
    post p 
WHERE 
    p.id = 1

INSERT INTO post (
    created_on, 
    title, 
    version, 
    id
)
VALUES (
    '2018-12-12', 
    'The High-Performance Java Persistence book is awesome!', 
    0, 
    2
)

INSERT INTO old_post (
    id, 
    title, 
    version
) 
VALUES (
    2, 
    'The High-Performance Java Persistence book is awesome!', 
    0
)

UPDATE 
    post 
SET created_on = '2018-12-12', 
    title = 'The High-Performance Java Persistence book is released!', 
    version = 1 
WHERE 
    id = 1 and version = 0

UPDATE 
    old_post 
SET title = 'The High-Performance Java Persistence book is released!', 
    version = 1 
WHERE 
    id = 1

Notice that both the entity insert and the update were properly replicated to the old_post table.

When deleting a Post entity:

entityManager.remove(
    entityManager.getReference(Post.class, 1L)
);

Hibernate is going to delete the old_post record prior to the post table row:

DELETE FROM 
    old_post 
WHERE 
    id = 1
    
DELETE FROM 
    post 
WHERE 
    id = 1 AND 
    version = 1

Awesome, right?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

The Hibernate event system mechanism is very handy when it comes to customizing the data access logic. As already explained, you can also use Hibernate event listeners to increment the version of a root entity whenever a child or grandchild record is inserted, updated or deleted.

While the Hibernate event listeners can track entity state transitions, SQL-level modifications that happen via native SQL queries or bulk update or delete statements cannot be intercepted. If you need a more generic way of tracking table record modifications, then you should use database triggers instead.

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.