How to intercept entity changes with Hibernate event listeners

(Last Updated On: January 18, 2019)
Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

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;

    //Getters and setters omitted for brevity
}

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:

<property name="hibernate.integrator_provider"
          value="com.vladmihalcea.book.hpjp.hibernate.listener.ReplicationEventListenerIntegrator"
/>

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?

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.

Download free ebook sample

Newsletter logo
10 000 readers have found this blog worth following!

If you subscribe to my newsletter, you'll get:
  • A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
  • 3 chapters from my book, High-Performance Java Persistence,
  • a 10% discount coupon for my book.

12 Comments on “How to intercept entity changes with Hibernate event listeners

  1. Great article!
    Can we make listener class to trigger for only one entity instead of triggering for all entities.

    • The listener is executed for any entity, but you can apply the filter in the listener and pass it to an entity-specific listener. Something like the Chain Pattern.

  2. Just my two cents….
    In my opinion and experience it is very rarely a good idea to use a hibernate listener to alter the database in any way.
    You can get really nested side effects and various kinds of problems.

    In you scenario above I would prefter the database trigger. 😉

  3. Hello,
    does the described method work for update queries, where we update multiple rows at same time? I mean will the provided listeners be called for the changed entities?

    • The event listeners work for entity state transition only. For bulk update or delete queries, you need to use database triggers.

  4. Very good post, Vlad!

    Do you think we could solve this issue using the standard JPA entity listeners (@EntityListeners)?

    If not, could you explain why?

    • It’s more difficult since you’d need a one-to-one association and sync both sides in every setter. It’s possible, but more intrusive and inefficient.

      • That’s what I thought!

        Another issue is that in a standard JPA entity listener we don’t have a reference to the current EntityManager (and, in turn, the current transaction). Maybe it’s possible to inject it via CDI with the new Hibernate versions, but I’m not completely sure.

      • Most JPA applications use Hibernate anyway, so that shouldn’t be an issue.

  5. Ah, I see how to get the old/new values by checking the dirty properties and using the getOldState/getState to retrieve them.

  6. Thanks for the post. Is it possible to retrieve the previous value in the update listener? One of the, in my opinion, big gaps in JPA is not to provide an interface for this. In every application I worked on the past 30 years there always has been a need to emit notifications with this info (besides create/delete) and I always have to write boiler plate code to support that.

    I had to implement the IntegratorProvider interface as well, by the way.

    • Yes, you can via the FlushEventListener. Check out the linked article at the end in the Conclusion section for an example.

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.

Hypersistence Optimizer can automatically detect if you are using JPA and Hibernate properly