How to Implement Conditional Auditing with Hibernate Envers


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Hibernate Envers automatically integrates with Hibernate ORM and provides a powerful and easy to use solution to write an audit log. As I described in a previous post, you just need to add Envers to your classpath and annotate your entities with @Audited. It will then document all insert, update and delete operations and you can even perform complex queries on your audited data.

Sounds quick and easy, right?

Well, it is, as long as you don’t want to use more than the default functionality. Unfortunately, that’s the case for most real-world applications. You most often need to store the name of the user who performed the changes or you just want to audit operations that meet certain conditions.

You can extend the information that are stored for each revision by extending the standard Revision entity. And don’t worry, that is much easier than it sounds.

But implementing a conditional audit requires more work. By default, Hibernate Envers registers a set of event listeners which are triggered by Hibernate ORM. You need to replace these listeners to customize Envers’ audit capabilities.

A Simple Example

Let’s take a look at an example. My application uses Book entity to store books in a database. As you can see in the following code snippet, I annotated this entity with @Audited so that Hibernate Envers audits all changes.

@Entity
@Audited
public class Book {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "id", updatable = false, nullable = false)
	private Long id;

	@Column
	private LocalDate publishingDate;

	...
	
}

But I don’t want to audit any update as long as the book doesn’t have a publishing date. That requires a few customizations to Envers’ event listeners.

Customize Envers’ Event Listeners

Hibernate Envers’ provides a set of listeners which are triggered by the following event types:

  • EventType.POST_INSERT
  • EventType.PRE_UPDATE
  • EventType.POST_UPDATE
  • EventType.POST_DELETE
  • EventType.POST_COLLECTION_RECREATE
  • EventType.PRE_COLLECTION_REMOVE
  • EventType.PRE_COLLECTION_UPDATE

In this example, I want to ignore all updates of books which are not published. These are all Book entities which’s publishingDate attribute is null. So, I will replace the existing listeners for events of EventType.PRE_UPDATE and EventType.POST_UPDATE.

Customize the Handling of EventType.PRE_UPDATE Events

That’s a lot easier than you might expect. Hibernate Envers provides the EnversPreUpdateEventListenerImpl. It already implements all the required logic to write the audit information. The only thing you need to do is to extend this class and ignore all update operations which you don’t want to document in the audit log.

I do that in the MyEnversPreUpdateEventListenerImpl class. It extends Envers’ EnversPreUpdateEventListenerImpl and overrides the onPreUpdate method. Within that method, I check if the event was triggered for a Book entity and if the publishingDate is null. If that’s the case, I ignore the event and in all other cases, I just call the method on the superclass.

public class MyEnversPreUpdateEventListenerImpl extends
		EnversPreUpdateEventListenerImpl {

	Logger log = Logger.getLogger(MyEnversPreUpdateEventListenerImpl.class
			.getName());

	public MyEnversPreUpdateEventListenerImpl(EnversService enversService) {
		super(enversService);
	}

	@Override
	public boolean onPreUpdate(PreUpdateEvent event) {
		if (event.getEntity() instanceof Book
				&& ((Book) event.getEntity()).getPublishingDate() == null) {
			log.debug("Ignore all books that are not published.");
			return false;
		}
		
		return super.onPreUpdate(event);
	}

}

Customize the Handling of EventType.POST_UPDATE Events

You can replace the listener for the EventType.POST_UPDATE event in the same way. The only difference is that you now need to extend the EnversPostUpdateEventListenerImpl class. I did that in the following code snippet.

public class MyEnversPostUpdateEventListenerImpl extends
		EnversPostUpdateEventListenerImpl {

	Logger log = Logger.getLogger(MyEnversPostUpdateEventListenerImpl.class
			.getName());

	public MyEnversPostUpdateEventListenerImpl(EnversService enversService) {
		super(enversService);
	}

	@Override
	public void onPostUpdate(PostUpdateEvent event) {
		if (event.getEntity() instanceof Book
				&& ((Book) event.getEntity()).getPublishingDate() == null) {
			log.debug("Ignore all books that are not published.");
			return;
		}

		super.onPostUpdate(event);
	}
}

Register your Listener Implementations

OK, you have custom listener implementations which ignore all changes to Book entities which don’t have a publishing date. You now just have to tell Hibernate Envers to use your listeners instead of the default ones.

You can do that by providing your implementation of Hibernate’s Integrator interface. The easiest way to do that is to copy and adapt the EnversIntegrator class. Hibernate Envers uses this class by default. All event listeners are registered in the integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) method.

In this example, I just want to replace the listeners for the EventType.PRE_UPDATE and EventType.POST_UPDATE events. So, I register my implementations instead of Envers’ default ones.

You can implement this part as it is required by your application. You can replace all of Envers’ listeners, don’t register listeners for some events or register listeners for other Hibernate EventTypes.

The following code snippet just shows the registration of the event listeners. Please take a look at the EnversIntegrator for more details about the implementation of the Integrator interface and of the integrate method.

public class MyEnversIntegrator implements Integrator {

	Logger log = Logger.getLogger(MyEnversIntegrator.class.getSimpleName());
	
	@Override
	public void integrate(Metadata metadata,
			SessionFactoryImplementor sessionFactory,
			SessionFactoryServiceRegistry serviceRegistry) {
		
		log.info("Register Envers Listeners");

		...

		if (enversService.getEntitiesConfigurations().hasAuditedEntities()) {
			listenerRegistry.appendListeners(
					EventType.POST_DELETE,
					new EnversPostDeleteEventListenerImpl( enversService )
			);
			listenerRegistry.appendListeners(
					EventType.POST_INSERT,
					new EnversPostInsertEventListenerImpl( enversService )
			);
			listenerRegistry.appendListeners(
					EventType.PRE_UPDATE,
					new MyEnversPreUpdateEventListenerImpl( enversService )
			);
			listenerRegistry.appendListeners(
					EventType.POST_UPDATE,
					new MyEnversPostUpdateEventListenerImpl( enversService )
			);
			listenerRegistry.appendListeners(
					EventType.POST_COLLECTION_RECREATE,
					new EnversPostCollectionRecreateEventListenerImpl( enversService )
			);
			listenerRegistry.appendListeners(
					EventType.PRE_COLLECTION_REMOVE,
					new EnversPreCollectionRemoveEventListenerImpl( enversService )
			);
			listenerRegistry.appendListeners(
					EventType.PRE_COLLECTION_UPDATE,
					new EnversPreCollectionUpdateEventListenerImpl( enversService )
			);
		}
	}

	...
}

The last thing you need to do to use your custom event listeners is to add the fully qualified name of your Integrator implementation in the META-INF/services/org.hibernate.integrator.spi.Integrator file.

org.thoughts.on.java.envers.MyEnversIntegrator

OK, that’s all. Let’s give it a try.

Write a Conditional Audit Log

The following test case persists a new Book entity without a publishingDate in the 1st transaction, updates the title in the 2nd transaction and sets the publishingDate in the 3rd transaction.

// Transaction 1 - Persist a new Book without a publishingDate
log.info("Transaction 1 - Persist a new Book without a publishingDate");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Book b = new Book();
b.setTitle("Hibernate Tips");
em.persist(b);

em.getTransaction().commit();
em.close();

// Transaction 2 - Update the title of the Book
log.info("Transaction 2 - Update the title of the Book");
em = emf.createEntityManager();
em.getTransaction().begin();

b = em.find(Book.class, b.getId());
b.setTitle("Hibernate Tips - More than 70 solutions to common Hibernate problems");

em.getTransaction().commit();
em.close();

// Transaction 3 - Set the publishingDate
log.info("Transaction 3 - Set the publishingDate");
em = emf.createEntityManager();
em.getTransaction().begin();

b = em.find(Book.class, b.getId());
b.setPublishingDate(LocalDate.of(2017, 04, 04));

em.getTransaction().commit();
em.close();

I only replaced the listeners for the EventType.PRE_UPDATE and EventType.POST_UPDATE events. Hibernate Envers, therefore, audits the INSERT operation of the 1st transaction. But you can see that in the 2nd transaction the custom event listener implementations ignored the update event. And when the 3rd transaction sets the publishingDate, the custom listeners delegate the event handling to Envers’ listener implementation.

11:28:44,266  INFO TestEnvers:94 - Transaction 1 - Persist a new Book without a publishingDate
11:28:44,524 DEBUG SQL:92 - select nextval ('hibernate_sequence')
11:28:44,597 DEBUG SQL:92 - insert into Book (publisherid, publishingDate, title, version, id) values (?, ?, ?, ?, ?)
11:28:44,623 DEBUG SQL:92 - select nextval ('hibernate_sequence')
11:28:44,627 DEBUG SQL:92 - insert into MyRevision (timestamp, userName, id) values (?, ?, ?)
11:28:44,630 DEBUG SQL:92 - insert into Book_AUD (REVTYPE, publishingDate, title, publisherid, id, REV) values (?, ?, ?, ?, ?, ?)

11:28:44,639  INFO TestEnvers:106 - Transaction 2 - Update the title of the Book
11:28:44,651 DEBUG SQL:92 - select book0_.id as id1_2_0_, book0_.publisherid as publishe5_2_0_, book0_.publishingDate as publishi2_2_0_, book0_.title as title3_2_0_, book0_.version as version4_2_0_, publisher1_.id as id1_7_1_, publisher1_.name as name2_7_1_, publisher1_.version as version3_7_1_ from Book book0_ left outer join Publisher publisher1_ on book0_.publisherid=publisher1_.id where book0_.id=?
11:28:44,673 DEBUG MyEnversPreUpdateEventListenerImpl:23 - Ignore all books that are not published.
11:28:44,674 DEBUG SQL:92 - update Book set publisherid=?, publishingDate=?, title=?, version=? where id=? and version=?
11:28:44,676 DEBUG MyEnversPostUpdateEventListenerImpl:23 - Ignore all books that are not published.

11:28:44,678  INFO TestEnvers:117 - Transaction 3 - Set the publishingDate
11:28:44,678 DEBUG SQL:92 - select book0_.id as id1_2_0_, book0_.publisherid as publishe5_2_0_, book0_.publishingDate as publishi2_2_0_, book0_.title as title3_2_0_, book0_.version as version4_2_0_, publisher1_.id as id1_7_1_, publisher1_.name as name2_7_1_, publisher1_.version as version3_7_1_ from Book book0_ left outer join Publisher publisher1_ on book0_.publisherid=publisher1_.id where book0_.id=?
11:28:44,682 DEBUG SQL:92 - update Book set publisherid=?, publishingDate=?, title=?, version=? where id=? and version=?
11:28:44,685 DEBUG SQL:92 - select nextval ('hibernate_sequence')
11:28:44,687 DEBUG SQL:92 - insert into MyRevision (timestamp, userName, id) values (?, ?, ?)
11:28:44,689 DEBUG SQL:92 - insert into Book_AUD (REVTYPE, publishingDate, title, publisherid, id, REV) values (?, ?, ?, ?, ?, ?)

Summary

Hibernate Envers documents all changes performed on the audited entities in the audit tables. You can change that by providing and registering your listener implementations.

The easiest way to implement a custom event listener is to extend Hibernate Envers’ regular listeners. They implement the required audit operations, and you can easily add your logic to exclude or customize the handling of certain entities. In this post, I did that to ignore all update operations on Book entities which’s publishingDate is null.

After you’ve implemented the custom listeners, you need to register them by implementing the Integrator interface and by adding the fully qualified name of your Integrator implementation in the META-INF/services/org.hibernate.integrator.spi.Integrator file.

That’s all for now. You can learn more about Hibernate Envers in the following posts:

2 Comments

  1. see this issue , i think it's not a good idea to register a listener for conditional auditing

    1. Avatar photo Thorben Janssen says:

      It looks like you missed providing a link to the issue.
      In my projects, the approach described in this article worked well. If you share your issue, I can take a look at it and provide some suggestions.

      Regards,
      Thorben

Comments are closed.