Monday, 18 February 2008
Hibernate Interceptor Gotchas
I'm trying to implement a Hibernate Interceptor to track user edits to an entity. I call this an EditInterceptor and it is similar to the AuditInterceptor that many people have described. I found implementing a Hibernate Interceptor in practice to have many "gotchas". These include:
Problem 1: Doing work in onSave() you discover the database id may not have been created yet (as the SQL insert hasn't occurred yet). Similarly, onFlushDirty() and onDelete() occur before the database has actually changed.
Solution: Process items in onFlush() using Interceptor instance collections to pass them from onSave(), onFlushDirty(), and/or onDelete(). Problem 2: You cannot modify objects in the session you are working with while it is flushing.
Solution: Create a second session in postFlush using the same jdbc connection. Problem 3: When implementing the solution to #2, you discover that session.connection() is deprecated, but that no alternative has been provided yet. Note that the Hibernate team doesn't practice proper use of deprecation here, as pointed out in the comments to HHH-2603
Solution: Use it anyway, and add @SuppressWarnings("deprecation"). Note that cloning a session from the same jdbc connection this way is safe, even if there are problems with using session.connection() to do ad hoc jdbc mischief. Problem 4: You're using Spring's Hibernate support and realize the Interceptor instance collections from Problem 1 aren't thread safe.
Solution: Wrap them in a thread local. See this example. Problem 5: Your updates are getting processed multiple times and you realize onFlushDirty is called multiple times, and read the javadocs and discover flush() doesn't always conclude with SQL syncronization to the database and that onFlushDirty() is still called in these cases.
Solution: Use a Set to collect changed entities and be sure to initialize this set in afterTransactionBegin(). Problem 6: You realize that in onFlushDirty, because of mulitple flushes as described above, that previousState and currentState are changing from call to call as you hit different points where hibernate automatically flushes. You need the true original state and the true final state.
Solution: Use a ThreadLocal map that associates an entity with the first previousState seen for it. Clear this map when a transaction starts and after you flush. Clear the updates in preFlush() and as usual process them in postFlush(), but when you calculate the update in onFlushDirty() do it relative to the originalState from the map, instead of previousState, which is only used to seed the map if it doesn't have a value for the entity.
Solution: Process items in onFlush() using Interceptor instance collections to pass them from onSave(), onFlushDirty(), and/or onDelete(). Problem 2: You cannot modify objects in the session you are working with while it is flushing.
Solution: Create a second session in postFlush using the same jdbc connection. Problem 3: When implementing the solution to #2, you discover that session.connection() is deprecated, but that no alternative has been provided yet. Note that the Hibernate team doesn't practice proper use of deprecation here, as pointed out in the comments to HHH-2603
Solution: Use it anyway, and add @SuppressWarnings("deprecation"). Note that cloning a session from the same jdbc connection this way is safe, even if there are problems with using session.connection() to do ad hoc jdbc mischief. Problem 4: You're using Spring's Hibernate support and realize the Interceptor instance collections from Problem 1 aren't thread safe.
Solution: Wrap them in a thread local. See this example. Problem 5: Your updates are getting processed multiple times and you realize onFlushDirty is called multiple times, and read the javadocs and discover flush() doesn't always conclude with SQL syncronization to the database and that onFlushDirty() is still called in these cases.
Solution: Use a Set to collect changed entities and be sure to initialize this set in afterTransactionBegin(). Problem 6: You realize that in onFlushDirty, because of mulitple flushes as described above, that previousState and currentState are changing from call to call as you hit different points where hibernate automatically flushes. You need the true original state and the true final state.
Solution: Use a ThreadLocal map that associates an entity with the first previousState seen for it. Clear this map when a transaction starts and after you flush. Clear the updates in preFlush() and as usual process them in postFlush(), but when you calculate the update in onFlushDirty() do it relative to the originalState from the map, instead of previousState, which is only used to seed the map if it doesn't have a value for the entity.
Posted by at 11:15 PM in stuff about java
