Hibernate version:
2.1.5
Mapping documents:
N/A
Full stack trace of any exception that occurs:
N/A
Name and version of the database you are using:
Oracle 9i
Debug level Hibernate log excerpt:
N/A
I've been back and forth with this issue over the past few weeks and have a 'solution' that I would like to garner opinions on. Please be gentle.
This is being used in a an application that will eventually go n-tier using a stateless session bean facade (which I'll call the interface a service). We've used the same design in the past where the client app (Swing based) would request some info from the ejb service which would open a session, load the date, close the session and return the Hibernate Objects themselves as the DTOs. (which would be serialized coming over the wire).
The client would then be allowed to modify the returned data (changing properties, adding removing items from collections) and then send it back to the service to save. All was good, new session would be created, info persisted, everyone was happy.
Now, our current situation is that we have a rather large Object graph (lots of associations which could potentially house LOTs of data). We've been fighting with coming up with an approach that would allow us to lazy load only the objects we're going to need as we need them.
This we had working fine when the object wasn't modified in any way shape or form and we could request data all day long using the session.Lock(ourObject, LockMode.NONE) calls.
Problems started to arise when we began to load up part of a 'full' object, modify some part of it and then try to lock it back into a session to load up some more data. Problems such as dirty collection, etc (all the fun stuff).
The proposed solution was then to use update() with a FlushMode of NEVER. Which did allow us to lock back into a session and get our data. However, it also triggered Interceptor (which we use for audit and comp keys), triggered sequence generation for unsaved objects, etc.
To get around some of this we tried cloning our objects, updating the clones and then pulling out the parts that we initialized and setting them back on our original object. Of course when we actually try to save the original later we run into issues such as 2 separate object instances referencing the same ID, shared collections, etc.
This is what the 'magic' looks like.
Code:
/**
* Loads the specified collections on the passed in object
*
* @param object
* @param properties a list of properties as <code>String</code>s
* @return
* @throws Exception
*/
protected HibernateDomainObject loadLazy(HibernateDomainObject object, List properties) throws Exception {
Session session = null;
try {
if (object != null && object.isSaved()) {
session = openSession();
object = loadLazy(object, properties, session);
}
return object;
} finally {
closeSession(session);
}
}
/**
* Loads the specified properties on the passed in rootObject
*
* @param rootObject the starting object to load properties for
* @param properties a list of properties as <code>String</code>s
* @param session an active Hibernate Session
* @return
* @throws Exception
*/
private HibernateDomainObject loadLazy(HibernateDomainObject rootObject, List properties, Session session) throws Exception {
if (rootObject != null && rootObject.isSaved()) {
//lock the object on the session
if (!session.contains(rootObject)) {
session.lock(rootObject, LockMode.NONE);
}
//iterate through the properties that are requested to be loaded
for (Iterator iterator = properties.iterator(); iterator.hasNext();) {
String property = (String) iterator.next();
Object subParent = null;
//check to see if the property contains an object chain
if (property.indexOf('.') > 0) {
int subParentIndex = property.indexOf('.');
//strip the name for the sub parent from the first element in the chain
String subParentProperty = property.substring(0, subParentIndex);
subParent = PropertyUtils.getProperty(rootObject, subParentProperty);
//create a property list that contains the rest of the chain (excludes the first element)
List subParentProperties = new ArrayList();
subParentProperties.add(property.substring(subParentIndex + 1, property.length()));
//initialize the subparent
if (!Hibernate.isInitialized(subParent)) {
System.out.println("Initializing " + subParent.getClass());
Hibernate.initialize(subParent);
//todo calling initialize on an object circumvents the Interceptor
}
//load the remaining properties using the subParent object as the rootObject by calling this method recursively
if (subParent instanceof HibernateDomainObject) {
subParent = loadLazy((HibernateDomainObject) subParent, subParentProperties, session);
} else if (subParent instanceof PersistentCollection) {
//todo note that this is only handling Sets for now as that is all we are using, may need to be enhanced later
//walk through each object in the collection and lazy load the properties for each one
for (Iterator it = ((Set) subParent).iterator(); it.hasNext();) {
loadLazy((HibernateDomainObject) it.next(), subParentProperties, session);
}
}
try {
//set the subParent (with all of it's lazy loaded properties) back on the rootObject
PropertyUtils.setProperty(rootObject, subParentProperty, subParent);
} catch (InvocationTargetException ite) {
//need to catch the InvocationTarget exception and check the root cause for a lazy init as the property change support may cause the exception and we want to ignore it
if (!(ite.getCause() instanceof LazyInitializationException)) {
throw ite;
}
}
} else {
Object value = PropertyUtils.getProperty(rootObject, property);
if (!Hibernate.isInitialized(value)) {
Hibernate.initialize(value);
}
try {
//set the value (with all of it's lazy loaded properties) back on the rootObject
PropertyUtils.setProperty(rootObject, property, value);
} catch (InvocationTargetException ite) {
//need to catch the InvocationTarget exception and check the root cause for a lazy init as the property change support may cause the exception and we want to ignore it
if (!(ite.getCause() instanceof LazyInitializationException)) {
throw ite;
}
}
}
}
}
return (HibernateDomainObject) rootObject;
}
The one thing I don't like about this is the fact that calling initialize on a proxied object doesn't make any calls to through the interceptor. (in other words, the object doesn't get flagged as being saved in my case - it came from the DB therefore it must be saved. the onLoad method in the interceptor does this typically but I can work around that. No big deal.)
Given the following sample simple model:
Code:
A-------<B>-------C-------<D
i.e. For a Root Object A, it has a collection of Bs that are lazy loaded, B many to 1 to C (may be proxied) and each C has a collection of Ds that are also lazy loaded.
In the above method I could call the lazy load for the following properties based on the root object A:
-B (which would initialize the collection of B)
-B.C (which would initialize the collection of Bs and then each C)
-B.C.D (which would initialize the collection of Bs, each C and also the collection of Ds off of each C)
From the client perspective I might call the method more than once. First time I might grab the B chain and then the second time in I might call the B.C.D chain. Again this all works fine if nothing has been modified in between calls.
However, if I initialize B collection, add a B to the collection and then try to load B.C.D, look out. (dirty collection).
Again, the call to update() breaks for a multitude of other reasons.
So, I trudge through some of the source and try to understand what goes on during a Lock call and start playing with the notion of possible modifying and applying a local patch to it.
There are 4 spots in code that I had to modify in order to get this to work for my scenario but I fear that I am going to break the internals horribly (However, the suite of Hibernate test cases DO pass with my code changes).
On to the changes:
My thought on the whole LockMode.NONE is that I don't really care if if I have a dirty object. I'm not going to update my object at this point (if I wanted to I could explicitly call update() anyways).
So, in the OnLockVisitor class I changed:
Code:
if ( coll.setCurrentSession(session) ) {
CollectionSnapshot snapshot = coll.getCollectionSnapshot();
if ( SessionImpl.isOwnerUnchanged( snapshot, persister, getKey() ) ) {
// a "detached" collection that originally belonged to the same entity
if ( snapshot.getDirty() ) {
throw new HibernateException("reassociated object has dirty collection");
}
session.reattachCollection(coll, snapshot);
}
else {
// a "detached" collection that belonged to a different entity
throw new HibernateException("reassociated object has dirty collection reference");
}
}
To:
Code:
if ( coll.setCurrentSession(session) ) {
CollectionSnapshot snapshot = coll.getCollectionSnapshot();
if ( SessionImpl.isOwnerUnchanged( snapshot, persister, getKey() ) ) {
// a "detached" collection that originally belonged to the same entity
//simply reattach the collection to the session. todo is there a way to check for a certain lock mode?
session.reattachCollection(coll, snapshot);
}
else {
// a "detached" collection that belonged to a different entity
throw new HibernateException("reassociated object has dirty collection reference");
}
}
The thing I don't like about this is that I can't specify a lock mode condition to ignore the dirty check (I assume it is there for a valid reason in the first place). However, if my collection IS dirty it simply gets reassociated with the session with the current valid snapshot. (i.e. it doesn't break cascade-all-delete-orphan) Also, it doesn't break any current unit tests.
The last piece of Hibernate code I had to touch was in the SessionImpl's lock method.
I changed the following:
Code:
EntityEntry entry = getEntry(object);
if (entry == null) {
final ClassPersister persister = getPersister(object);
final Serializable id = persister.getIdentifier(object);
if (!isSaved(object)) {
throw new HibernateException("cannot lock an unsaved transient instance: " + MessageHelper.infoString(persister));
}
entry = reassociate(object, id, persister);
cascading++;
try {
Cascades.cascade(this, persister, object, Cascades.ACTION_LOCK, Cascades.CASCADE_ON_LOCK, lockMode);
} finally {
cascading--;
}
}
To:
Code:
EntityEntry entry = getEntry(object);
if (entry == null) {
final ClassPersister persister = getPersister(object);
final Serializable id = persister.getIdentifier(object);
if (!isSaved(object)) {
if (lockMode.greaterThan(LockMode.NONE)) {
throw new HibernateException("cannot lock an unsaved transient instance: " + MessageHelper.infoString(persister));
} else {
return; //simply return, don't want to lock the transient instance
}
}
entry = reassociate(object, id, persister);
if (lockMode.greaterThan(LockMode.NONE)) {
cascading++;
try {
Cascades.cascade(this, persister, object, Cascades.ACTION_LOCK, Cascades.CASCADE_ON_LOCK, lockMode);
} finally {
cascading--;
}
}
}
The above simply does a check to see if I'm in a LockMode none scenario and if it encounters an unsaved instance it simple doesn't try to do anything else with it. If the object hasn't been saved then it can't have anything to lazy load off of it.
The second piece with the cascades (and I'm REALLY not sure how truly bad this is) is to get around the fact that even when calling a LockMode.NONE it will try to initialize any non-initialized proxied objects. It kind of eliminates my control over what I want to grab. If you notice in my 'magic' method I'll simply initialize the proxies if the user requested them. Otherwise I don't care about them at this point.
Again, this doesn't break anything in unit testing.
The one thing that I could change in this is to add another LockMode lower than NONE (DIRTY, UNCOMMITTED, BOB) and use that in the greater than checks, that way the NONE would still work as originally designed. Although that would require modifying the LockMode class as well.
What am I missing? I'm having a hard time believing that we're the only development team in the world using Hibernate with the desire to do something like this but maybe it is an unrealistic goal to achieve.
I'm perfectly content with patching future releases of Hibernate in the near future for this but I just want to make sure I'm not going to knacker myself in some unknown manner.