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.
@vlad_mihalcea hi! I'm a regular reader of your tweets & blogs, and have a problem I'd really appreciate if you could cast your eyes over! Very much appreciated 🙂
— Mitch Kent (@mbot_spud) December 11, 2018
Rename a table in a backwards compatible way using JPA and Postgres (i.e. duplicate/alias)https://t.co/4uLvBUWeoO https://t.co/52WMVBYMLp
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.
How to intercept entity changes with #Hibernate event listeners @vlad_mihalceahttps://t.co/crSOQ1UZZa pic.twitter.com/ava8qkovB3
— Java (@java) December 16, 2018
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:
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 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?
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.
