Wednesday, 21 May 2008

Fun with Hibernate's flush mode

An "interesting" problem we ran into recently was that even though we discarded changes to a domain object (when it failed validation) some of those changes were actually getting written to the database.

Consider this example:
class Lolcat {

String name
int number
boolean isGood

static constraints = {
number(nullable: true)

and let's re-create the problem:
    // save an instance
def kitteh = new Lolcat(name: 'ceiling cat', isGood: true)
assert true)

// check hibernate session contains our object and is not dirty
assert sessionFactory.currentSession.contains(kitteh)
assert !sessionFactory.currentSession.isDirty()

// update a simple property = 'basement cat'

// session is now dirty as kitteh has pending writes
assert sessionFactory.currentSession.isDirty()

// assign another property based on a count query
kitteh.number = Lolcat.countByName( + 1

// as we'd expect session is still dirty
assert sessionFactory.currentSession.isDirty()

// update another simple property
kitteh.isGood = false

// change our mind and discard changes (as real code would if validation failed)

// session is no longer dirty
assert !sessionFactory.currentSession.isDirty()

// re-fetch our instance from the database and check it wasn't updated
assert kitteh.isGood
assert == 'ceiling cat' // FAILS - value is 'basement cat' WTF?!

What the devil is going on there? One of the property values we thought we'd discarded has been written to the database after all. We've been checking the dirty state of the Hibernate session as we go and it didn't look like it was flushed anywhere along the way.

We can shed some light on the problem by splitting up the assignment of the 'number' property like this:
    def newNumber = Lolcat.countByName( + 1
assert sessionFactory.currentSession.isDirty() // FAILS (and a lightbulb goes on)
kitteh.number = newNumber
assert sessionFactory.currentSession.isDirty()

Now we can see that what's happening is the count query we execute is actually causing the Hibernate session to get flushed, hence the 'name' property value we assigned before the query gets prematurely written to the database while the 'isGood' property we assigned after the query gets discarded properly.

The culprit here is the Hibernate flush mode. By default it's set to AUTO. The Hibernate javadocs (for those of you too lazy to look it up) tell us: "The Session is sometimes flushed before query execution in order to ensure that queries never return stale state."

There are two ways to solve this then. One solution is to set the flush mode to MANUAL or COMMIT (the latter would be more appropriate in a transactional context such as a service method or withTransaction block), wrapping the code that modifies the domain object instance in something like this:
    try {
sessionFactory.currentSession.flushMode = org.hibernate.FlushMode.MANUAL
// modify and save our object
} finally {
sessionFactory.currentSession.flushMode = org.hibernate.FlushMode.AUTO

Which will certainly make the example above work. However, a better option in my opinion may be to do any pre-processing queries before starting to assign new values to the object. That way the session won't get flushed prematurely and we'd retain the benefits of FlushMode.AUTO.


Dread Pirate Rob said...
This comment has been removed by the author.
Gus said...

great post Rob

SteveC said...

You see this problem lots when updating unique fields such as mobile numbers / email addresses if you use the domain object as a form bean. Explicitly searching for the unique field flushes the domain object to the database so you always get at least one match. Good old hibernate.

paul campbell said...

Well ... IMO hibernate behaves perfectly logically - the "problem" is that grails treats a non-flushed session as kind of transaction-within-a-transaction where you can selectively roll objects back using 'discard'.

I.e. writing code that relies on session state being different to database state is just wrong :-)

Dread Pirate Rob said...

It's not anything Grails does that causes that behaviour. That's what the Hibernate Session *is* - a read/write buffer that prevents excessive database access at the price of risking stale state. GORM is a *very* thin layer over Hibernate.