Monday, July 31, 2006

Hibernate events

I'm always having an argument with myself about how much intelligence to put in the database. I have a strong preference: none. I would rather work in Java and just let the database store data. But I always make an exception for database-generated identifiers, and then I start thinking that maybe I should write a trigger to let the database take care of timestamps, or recalculate a photo's total rating in photoSIG when someone ads a new critique, or...

And then I start thinking that maybe I should do the opposite, remove the database-generated IDs, use UUIDs for everything, and really use the database just for storing data.

For lots of domain objects I maintain two fields, "created" and "updated." You can probably figure out what they do. Throughout the photoSIG code I am forever updating the updated field. I decided to investigate whether I can use Hibernate to do this for me. I only really care about when the object was written to the database, not when it was last updated in the code (I only keep these objects around for a fraction of a second, the duration of a web request), so I figured that I could use a Hibernate event to wait for the object to be flushed and then update the updated field at that time. Or so I thought.

I've never had any love for the Hibernate Interceptor interface. It seems very low-level. It provides you with the object under consideration, but it also sends you the fields from the entity in parallel arrays. If you want to update anything, you have to update the arrays, meaning that your Interceptor implementation not only has to know a lot about Hibernate but also has to know a lot about the structure of your object too. It's not pretty.

And so it was with relief and anticipation that I approached the new Hibernate events API. But if anything, the Hibernate events API seems even more low-level than the Interceptor interface. The way that Hibernate works now is that essentially every method you call on Session produces an event that gets handled by a chain of event listeners. You can add your own listeners, but the last listener in the chain better be the Hibernate default listener for that event, otherwise Hibernate will behave very strangely or just won't work at all. The "events" API is really an implementation of the strategy pattern. This is unfortunate, since I would have preferred an API that specified a useful contract between Hibernate and an application, rather than an internal contract between Hibernate and Hibernate.

Regardless of my gripes about the API itself, the thing that really got in my way was the lack of documentation concerning when the "events" are produced and where I should put my code to achieve some desired effect. The JavaDocs aren't much help. The class description for LoadEventListener is "Defines the contract for handling of load events generated from a session," and the documentation for its single method, onLoad, reads "Handle the given load event." Is this called right when someone calls load, or after Hibernate instantiates the object, or after the object is populated, or what? There is also a PreLoadEventListener and a PostLoadEventListener. What's the difference? The quality of the Hibernate documentation overall is excellent, but in the case of the events API, it's just not there.

I pressed on. I created an implementation of FlushEntityEventListener and, in onFlushEntity, set the updated field. But when I tested this implementation by creating a new object, Hibernate attempted to insert null into the updated column in the database. Huh? It turns out that Hibernate schedules the insert as soon as the application calls If you update the object in the onFlushEntity method, then Hibernate schedules an update. Meanwhile the original insert fails.

Obviously I had to intercept the save "event" and update the object before Hibernate scheduled the insert. I created an implementation of SaveOrUpdateEventListener that updated the updated field and registered it using EventListeners.setSaveOrUpdateEventListeners. But when I ran the test again, nothing changed! Now I was really confused. I had to USTL to figure out that the setSaveOrUpdateEventListeners sets the listeners that are called when the app calls Session.saveOrUpdate, not or Session.update. I had to call setSaveEventListeners, which still accepts a SaveOrUpdateEventListener.

Now Hibernate inserted the row with a non-null value for updated. But I still had some problems. The updated value that I set in my SaveOrUpdateEventListener wasn't the time at which the object was written to the database but rather the time at which it was handed to Hibernate. A fine distinction, but either you implement the requirements that you've defined for yourself or you don't. FlushEntityEventListener still updates the updated field when the object is flushed, so the "save" time is immediately overwritten by the "flush" time, but Hibernate has to issue an update in order to do it.

After poking around some more, I started thinking that the Session-interface "events" that I was using were the wrong way to go and that I should look into PreInsertEventListener, which is apparently called just before Hibernate inserts something into the database. Unfortunately the corresponding PreInsertEvent holds an array of field values, just like the methods of the Interceptor interface. It's too late to change the object there.

At that point, I abandoned my attempt to get Hibernate to maintain the updated field.

I understand the motivation behind the design of the events API. It's not that I don't get it. But I wish that the Hibernate team had given us something simpler and well-documented. All I really want is a callback from Hibernate saying, "Hey, I'm about to write this object to the database, so if there are any last-minute updates you want to do, this would be a good time!" In other words, what I want from Hibernate is a trigger.


At 7:42 AM, Blogger jack said...

Hi Willis.

Well, you just described exactly the pain I am going through now - 3 years later. I actually started with the flush-related listeners (PreInsertEventListener and PreUpdateEventListener) and now I am moving more toward the session-related listener of SaveOrUpdateEventListener. Your trial and error there explained a lot to me regarding the very inconsistent absence of a SaveEventListener (if you have a saveupdate listener then I would think you would also have separate save and update listeners as well). But you got around that and explained it nicely.

Back to the flush-related PreInsertEventListener. In that case though, it's NOT too late to update the field. It's just that the API doesn't provide an easy way to update the field. Plus there's zero documentation around it. This post describes how to update a field and have it actually move on to the db.

However, now that I am armed with all this I think your original approach was the right way to do it. I am really not sure what the flush-related listeners buy us since we can't easily update objects before SQL execution - of course the Hibernate team won't tell us either.

Thanks for sharing this information.

At 4:38 PM, Blogger jhb said...

Thank you! I was just going through exactly the same process you did. It's nice to have my thoughts and research validated!


Post a Comment

<< Home