-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 2 posts ] 
Author Message
 Post subject: Lazy Loading disconnected objects (allowing dirty)
PostPosted: Fri Aug 06, 2004 6:50 pm 
Expert
Expert

Joined: Sat Jan 17, 2004 2:57 pm
Posts: 329
Location: In the basement in my underwear
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.


Top
 Profile  
 
 Post subject:
PostPosted: Mon Aug 09, 2004 12:28 pm 
Expert
Expert

Joined: Sat Jan 17, 2004 2:57 pm
Posts: 329
Location: In the basement in my underwear
Alright, if anyone read this and cared I cleaned up my 'magic' methods.

Here's the current working version. Simply pulled out some common code into a new method.

Code:
    /**
     * 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);

                    //initialize (if applicable) the subParent and set it on the rootObject
                    initializeAndSet(rootObject, subParent, 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()));

                    //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);
                        }
                    }
                } else {
                    Object value = PropertyUtils.getProperty(rootObject, property);
                    initializeAndSet(rootObject, value, property);
                }
            }
        }
        return (HibernateDomainObject) rootObject;
    }

    /**
     * This will initialize a hibernate object <code>HibernateDomainObject</code> or <code>PersitentCollection</code> and then set it back on a root object
     *
     * @param rootObject
     * @param objectToInitialize
     * @param property
     * @throws Exception
     */
    private void initializeAndSet(HibernateDomainObject rootObject, Object objectToInitialize, String property) throws Exception {
        if (!Hibernate.isInitialized(objectToInitialize)) {
            Hibernate.initialize(objectToInitialize);
        }

        try {
            //set the value (with all of it's lazy loaded properties) back on the rootObject
            PropertyUtils.setProperty(rootObject, property, objectToInitialize);
        } 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;
            }
        }
    }


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 2 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.