Thursday, April 7, 2011

Accessing the identifier of a lazy loaded association in grails without another database call

I ran into a bit of a weird scenario recently with grails. I have a couple of classes similar to those below:

class Foo {
Bar bar
}

class Bar {
static hasMany = [foos: Foo]
}


Essentially, I have a many to one relationship. Now, consider the following code:

Foo foo = Foo.get(1)
Bar bar = foo.bar


Assuming that there is actually a bar set on this particular foo, what will you get?

You'll get the actual Bar, completely instantiated. Now, imagine the exact same scenario in normal java with the exact same equivalent hibernate settings via annotations. (I'm too lazy to model what that would look like) What would you get back for a similar call? You would get back a proxy! Then, if you called something on the proxy, hibernate would go and fetch the actual object for you.

Gorm unwraps the proxy when the object itself is called via a property.

If you take a look at the HibernatePluginSupport class, which configures all of the dynamic Gorm methods (and the first place I go when I want to see how a particular method is actually working) you will see this in action:

static final LAZY_PROPERTY_HANDLER = { String propertyName ->
def propertyValue = PropertyUtils.getProperty(delegate, propertyName)
if (propertyValue instanceof HibernateProxy) {
return GrailsHibernateUtil.unwrapProxy(propertyValue)
}
return propertyValue
}

/**
* This method overrides a getter on a property that is a Hibernate proxy
* in order to make sure the initialized object is returned hence avoiding
* Hibernate proxy hell
*/
static void handleLazyProxy(GrailsDomainClass domainClass,
GrailsDomainClassProperty property) {
String propertyName = property.name
def getterName = GrailsClassUtils.getGetterName(propertyName)
def setterName = GrailsClassUtils.getSetterName(propertyName)
domainClass.metaClass."${getterName}" =
LAZY_PROPERTY_HANDLER.curry(propertyName)
domainClass.metaClass."${setterName}" = {
PropertyUtils.setProperty(delegate, propertyName, it)
}

for (GrailsDomainClass sub in domainClass.subClasses) {
handleLazyProxy(sub, sub.getPropertyByName(property.name))
}
}
So, anytime you access your properties, the above code is going to unwrap it.

What whoever wrote this is trying to avoid is the 'proxy hell' mentioned in the comment. The hibernate docs have a good explanation of it:

http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html#performance-fetching-lazy

As does the Grails docs:

http://grails.org/doc/latest/guide/5.%20Object%20Relational%20Mapping%20%28GORM%29.html#5.5.2.8%20Eager%20and%20Lazy%20Fetching

They're more eagerly fetching the lazy relationships than Hibernate does to avoid some of the instanceof check issues outlined in the above links.

Now, back to my issue. What happens if you want to obtain the identifier of your relationship without actually resulting in a database call to get it? The proxy has the identifier already, that's how it loads it up when you call it. In pure hibernate there's a few ways to do it:
  1. Session.getIdentifier
  2. entity.getId() (assuming you are using property based configuration)
  3. Cast it to a HibernateProxy, get the LazyLoadInitializer and get the Identifier from it
The problem with all of these solutions is that you have to get around the Gorm code above that unwraps any properties when you call a getter. You can't just add setBar and getBar. However, you can add another method that can get 'raw' access to the field. And before anyone posts a comment about it you can't use load either because it requires the id, and that's the whole point of this exercise (getting the id that is).

I didn't find this out until I had wasted quite a few hours, but apparently this was a known issue: http://jira.grails.org/browse/GRAILS-2570 I never saw this when googling for it originally, but you can essentially say:

foo.barId


And you will get the id of the bar without the proxy being unwrapped. I'm not sure this is referenced anywhere in the docs though.

3 comments:

Amit said...

Thanks for posting this!

Manuel said...

Great post, thanks! Already bookmarked this for future reference!

timbonicus said...

Thanks for putting this information out there, you saved me a few hours of frustration!