Thursday, April 24, 2008

How to: Always eagerly load objects from database with Hibernate 3 (without changing configuration xmls)

So it was getting annoying to debug my app. with Hibernate 3 for it's 'lazy load everything by default' feature. Every time I wanted to see what's the value of some collection I will see these weird CGLIB proxy members lurking in debug views.

CGLIB$CALLBACK_0    org.hibernate.proxy.pojo.cglib.CGLIBLazyInitializer@9be3aa
class$net$sf$cglib$proxy$InvocationHandler interface net.sf.cglib.proxy.InvocationHandler
class$net$sf$cglib$proxy$NoOp interface net.sf.cglib.proxy.NoOp
class$org$hibernate$proxy$pojo$BasicLazyInitializer null
componentIdType null
constructed true

For every association I would either evaluate the code or create expressions to see what's coming from DB. Lazy loading makes debugging more slower than it already is. What's the big deal?, you will say, Just go ahead and change configuration to lazy-load = "false". Well, I can do it but changing more than five hundred .hbm.xml files will help me more with retiring early than fixing the problem at hand.

So before it becomes nightmare for me I thought of hacking hibernate to make it eagerly load entire object from database. I'm saying hacking because there's no API in Session or SessionFactory interfaces that would allow you to unwrap these un-dead proxies automatically.

Here's how you can hack hibernate to prevent lazy loading:

The basic idea of making hibernate do what you want is to invert it's assumption, i.e. make everything load eager by default, we'll plug ourself to Mappings class and will force eager load implicitely. Follow these steps:

1. First create a class in org.hibernate.cfg package which extends Mappings:

package org.hibernate.cfg;

import java.util.List;
import java.util.Map;
import org.hibernate.MappingException;
import org.hibernate.mapping.PersistentClass;

public class MyMapping extends Mappings {


public MyMapping(Map classes, Map collections, Map tables, Map queries, Map sqlqueries, Map sqlResultSetMappings,
Map imports, List secondPasses, List propertyReferences, NamingStrategy namingStrategy,
Map typeDefs, Map filterDefinitions, Map extendsQueue, List auxiliaryDatabaseObjects,
Map tableNamebinding, Map columnNameBindingPerTable) {

super(classes, collections, tables, queries, sqlqueries, sqlResultSetMappings, imports, secondPasses,
propertyReferences, namingStrategy, typeDefs, filterDefinitions, extendsQueue, auxiliaryDatabaseObjects,
tableNamebinding, columnNameBindingPerTable);

}

@Override
public void addClass(PersistentClass persistentClass) throws MappingException {

persistentClass.setLazy(false); // No Proxies, load everything in one shot
super.addClass(persistentClass);

}

}

The 'addClass' method achieves all we need, just make persistenceClass non-lazy, it would be lazy by default.

2. Now hook this class with Configuration class.
    public class MyConfiguration extends Configuration {
@Override
public Mappings createMappings() {
return new MyMapping(classes, collections, tables, namedQueries, namedSqlQueries, sqlResultSetMappings,
imports, secondPasses, propertyReferences, namingStrategy, typeDefs,
filterDefinitions, extendsQueue, auxiliaryDatabaseObjects, tableNameBinding,
columnNameBindingPerTable);
}
}

There you go, You can now create session factory out of it and you will get fully debug-compatible Java objects loaded from db.

This was way easier to achieve than I could think hooking hairy SessionImpl beast and making org.hibernate.event.LoadEventListener.LoadType INTERNAL_LOAD_EAGER from INTERNAL_LOAD_LAZY without changing SessionImpl itself.

Other than that, I found some talking about lazy loading being problematic with XStream serialization. Threre's a JIRA defect for XStream relating to this as well. I faced the same few weeks before when I thought of removing hibernate from test cases, XStream basically throws up when it finds a confusing CGLIB proxy like this:

com.thoughtworks.xstream.converters.ConversionException: Cannot handle CGLIB enhanced proxies with multiple callbacks
at com.thoughtworks.xstream.converters.reflection.CGLIBEnhancedConverter.marshal(CGLIBEnhancedConverter.java:85)
at com.thoughtworks.xstream.core.AbstractReferenceMarshaller.convert(AbstractReferenceMarshaller.java:55)
at com.thoughtworks.xstream.core.TreeMarshaller.convertAnother(TreeMarshaller.java:50)
at com.thoughtworks.xstream.core.TreeMarshaller.start(TreeMarshaller.java:73)
at com.thoughtworks.xstream.core.ReferenceByXPathMarshallingStrategy.marshal(ReferenceByXPathMarshallingStrategy.java:34)
at com.thoughtworks.xstream.XStream.marshal(XStream.java:765)
at com.thoughtworks.xstream.XStream.marshal(XStream.java:754)
at com.thoughtworks.xstream.XStream.toXML(XStream.java:735)
at com.thoughtworks.xstream.XStream.toXML(XStream.java:725)

I faced this problem before as well and now I don't have to worry about it anymore. You guys can probably just go ahead and load your objects as I described above and your serialization would work fine.