Showing posts with label hibernate. Show all posts
Showing posts with label hibernate. Show all posts

Wednesday, 15 April 2009

instanceof vs. HibernateProxy and inheritance

I posted something over on my blog about the exciting things that can happen when Hibernate proxies your base class to wrap an instance of your sub-class.

Wednesday, 11 February 2009

Case-insensitive ordering using HSQLDB

We've recently begun the process of testing our app on Postgres, and came across a problem where our 'order' statements were returning results in a different order to HSQLDB.

Postgres was ignoring the case of items, whereas HSQLDB was not - so if I were to order the strings "Al" and "AM", HSQLDB would show "AM" before "Al", but Postgres would show "Al" before "AM".

Given that the behaviour of Postgres (case-insensitive ordering) was the desired behaviour, we wanted to get HSQLDB to do the same. After a lot of searching any many dead ends, we found that you simply use ".ignoreCase()" after an order criteria:

List books = Book.withCriteria() {
ilike('authorName', "%${searchKey}%")
order('authorName', 'asc').ignoreCase()
maxResults(max)
}

Friday, 4 July 2008

Criteria queries + associations = confusion


Since we've been bitten by this problem twice in the same week now I thought it was worth a post.

Consider these classes:
    class Author {
String name
static hasMany = [books: Book]
}

class Book {
String name
static belongsTo = [author: Author]
}

Then let's attempt to find all authors who have written a book with a particular name:
    Author wg = new Author(name: 'William Gibson')
wg.addToBooks new Book(name: 'Pattern Recognition')
wg.addToBooks new Book(name: 'Spook Country')
assert wg.save(flush: true)

sessionFactory.currentSession.clear()

List authors = Author.withCriteria {
books {
eq('name', 'Spook Country')
}
}
assertEquals 1, authors.size()
assertEquals 2, authors[0].books.size() // FAILS result is 1

When you look at how Hibernate implements this query under the hood it's reasonably clear what's going on. It will be doing something roughly along the lines of:
    select * from author a, book b where a.id = b.author_id and b.name = ?

The problem is it then makes the mistake of thinking that the books collections of the author objects it has found can be initialised based on that result set.

Where this problem has bitten us is when objects fetched by such a query are also used by an unrelated bit of code that doesn't bother reading from the database as it quite reasonably thinks (some of) the objects is needs are in the Hibernate session already. Unfortunately, the view module related to this other bit of code is reliant on the elements of the collection that weren't loaded by our defective query. I'm sure you can imagine the fun to be had debugging that kind of problem.

I've been hacking around with the criteria attempting various permutations such as:
    // DOES NOT WORK
List authors = Author.withCriteria {
'in'('books.name', 'Spook Country')
}

and
    // ALSO DOES NOT WORK
List authors = Author.withCriteria {
'in'('books') {
eq('name', 'Spook Country')
}
}

But I've got nowhere yet. The best I've managed to do is:
    List authors = Author.withCriteria {
books {
eq('name', 'Spook Country')
}
}
// if you have a weak stomach look away now
authors*.refresh()

If you're wondering, yes that is going to execute n+1 selects.

Another option is to give up and use an HQL query.

This problem applies equally to one-to-many (and presumably many-to-many) associations using Set, List or Map.

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 kitteh.save(flush: true)
sessionFactory.currentSession.clear()
kitteh.refresh()

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

// update a simple property
kitteh.name = '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(kitteh.name) + 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)
kitteh.discard()

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

// re-fetch our instance from the database and check it wasn't updated
sessionFactory.currentSession.clear()
kitteh.refresh()
assert kitteh.isGood
assert kitteh.name == '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(kitteh.name) + 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.

Tuesday, 29 April 2008

What happens when you throw exceptions from services


class LolController {
def sessionFactory
def lolService
def gimmehCheezburger = {
def kitteh = Lolcat.get(params.id)
try {
lolService.canHasCheezburger(kitteh)
assert sessionFactory.currentSession.contains(kitteh)
} catch (EpicFailException wtf) {
assert !sessionFactory.currentSession.contains(kitteh)
}
render(view: 'lolcat', model: [lolcat: kitteh])
}
}

Note that the domain object 'kitteh' becomes detached from the hibernate session if the service throws an exception. This would cause reads of the object's lazy-loaded associations in the view rendering phase to fail.

If this is really what you want to do (it looks unpleasantly like controlling program flow via exceptions) you can use kitteh.refresh() in the catch block to re-attach the object to the hibernate session.