Saturday, October 11, 2008

Hibernate Interceptors, Events and JPA Entity Listeners

Hibernate became is an implementation of the JPA specification. The JPA specification is part of the EJB3 specification (JSR-220). See, SUNs FAQ on that if you want to know more about that.

Hibernate Interceptors
In a recent project I wanted to intercept Create, Update and Delete events. From the Hibernate Reference Documentation it was pretty straightforward to find out how to create an Interceptor class. I extended my AuditInterceptor class from the EmptyInterceptor interface and implemented the onSave, onFlushDirty and onDelete methods but then was a bit puzzled how to activate the interceptor. Chapter 2 talks about specific hibernate configuration properties, but how to use those when you're using a JPA persistence.xml? It turns out the answer is very simple. Pretty much any vendor specific property can be set in the persistence.xml using a properties block. In this case a property with name hibernate.ejb.interceptor needs to be set to the full name of my AuditInterceptor class.

Some advantages are that
  • you don't to change any of your existing code, just add your interceptor and update the persistence.xml
  • the onFlushDirty gives you a nice detailed information on what changed
but on the flipside
  • the interceptor fires for every entity bean, and chances are you're only interested in a subset. I ended up doing the filtering in my AudutInterceptor.
  • if your primary key is set by the database, the id is still undefined when onSave fires.
Hibernate Events
The capabilities of Hibernate Events API goes beyond those of the Interceptors. It gives you very fine-grained control over where to hook into the event stream. Check out the org.hibernate.event package for all the different interfaces you can implement.

Some advantages
  • fine-grained control over where to hook into the persistence process (lots of Pre- and Post-event interfaces)
  • The event objects are contain a wealth of information, including previous and current state of the entity.
but on the flipside
  • hooking up your EventListener is either done programmatically or using the Hibernate session-factory configuration block. I guess I could use a hibernate.cfg.xml file and reference that in my persistence.xml, but it started to feel convoluted to me.
JPA EntityListeners
FInally there are non vendor specific JPA EntityListeners. To use an EntityListener you simply add a '@EntityListener (class=)' annotation. Then in your listener class you can use other annotations (like @PrePersist, @PreRemove, @PostPersist, @PostRemove, @PreUpdate, @PostUpdate and @PostLoad) to decorate your listener methods.

Some advantages are
  • fine-grained control as to which entities you want to listen to (annotation on the entity, referencing your listener class)
  • pretty good control on where you want to hook in your listener (annotation on a method on the listeners class)
but on flipside
  • the API only gives you the current state of the entity, and does not provide information as to what changed in case of an update.
Conclusion
In my case I ended up with a hybrid solution using a HibernateInterceptor for the Update and Delete, and using EntityListeners for the Create event, listening to the PostInsert events so that the id field on my entities are defined.

8 comments:

Anonymous said...

How do we get a handle to the EM/DAO inside of the interceptor callback when the interceptor class is defined in the persistence.xml?

thanks.

Kurt Stam said...

As far as I understand it you're not supposed to do any db calls in the interceptor. I ended up only capturing the entity itself, and the IDs of the related objects, as entity.getChildObject.getId() does not need to load up the ChildObject. If you do attempt to load up more objects, you may run into "no session" exceptions, if the object was loaded in a transaction that is no longer open. Which in turn will mark the current transaction for rollback.

This obviously limits the power of interceptor pretty severly. If you object is HibernateProxy you can get to some extra information using:

if (object instanceof HibernateProxy) {
HibernateProxy proxy = (HibernateProxy) object;
implementationClass = proxy.getHibernateLazyInitializer().getImplementation().getClass();
}

Well I'm interested to hear what you find out.

Hope this helps you.

--Kurt

Chris said...

Kurt,

So then would you say logging object metadata (time, user) and writing this data to the database would be a *bad* example of proper usage of these interceptors? I'd like to build a "global" logging function that would extend to all entities without needing to define this logic for each entity. I've been looking around for solutions and I'm thinking of using either @PostPersist or interceptors. Which would you suggest?

Thanks for the useful information.

Kurt Stam said...

I think as long as you make sure you open a new session you should be ok. You're just need to very careful not to intercept that new transaction, so you may want to consider dropping a message on an event queue and then writing a service that listens to this event queue to do your logging.

Julien Kronegg said...

For JPA Entity Listeners, under OpenJPA, you can get the previous entity state.

naaka said...

I have modified the session impl to generate the id also for objects instanciated from any interceptor:

public Object instantiate(EntityPersister persister, Serializable id) throws HibernateException {
errorIfClosed();
checkTransactionSynchStatus();
Object result = interceptor.instantiate( persister.getEntityName(), entityMode, id );
if ( result == null ) {
result = persister.instantiate( id, entityMode );
} else {
persister.setIdentifier(result, id, entityMode);
}
return result;
}

for my use case seems to work pretty well.

Is there a reason why user should control also the setting of the id through an interceptor ?

To me no....

naaka said...

Regarding Hibernate Interceptors, i have posted the following hibernate post.


what do you think ?

Anonymous said...

interesting summary, thanks!

i've found two other options to configure hibernate Event Listeners in JPA environment

1) either directly in presistence.xml in the properties element the same way you configure the Interceptor, with property names "hibernate.ejb.event.post-update" and so on.

this way hibernate instantiates the listeners on startup.

2) or provide your own PersistenceProvider - subclass org.hibernate.ejb.HibernatePersistence and inject the EventListeners directly to the Ejb3Configuration before the entity manager factory is built.

this way you (or e.g. spring) can manage the EventListener instances. however this is very tightly coupled to the hibernate configuration implementation i'm using(3.4.0.GA) and it may happen that your event listeners won't be properly initialized (e.g. CallbackHandlerConsumer
and JACCSecurityListener implementations, check org.hibernate.ejb.EventListenerConfigurator.configure()).