Wednesday, March 5, 2014

How to finally solve the Hibernate Lazy Load problem: Make the Collection reconnectable


The problem

After years and years working with hibernate there are a few things I hate about it.

The logging and the error messages are not clear and sometimes misleading (I will not expound now) and then the most common problem of all, the lazy load exception.

There are solutions like SessionInView pattern etc, but I and thousands of others wonder why it is so complicated or bad to make the Collection reconnect itself when needed.

I mean eclipse link does it also with a readonly connection.

The solution

So here is my solution and it finally seems to work. I actually implemented Collection class that does the reconnecting for me.

For this to work i have to do several steps

In the hibernate mapping define field access.


You can do this with annotations or if there is hibernate mapping file it looks like this.

At the top do:
    <hibernate-mapping default-access="field">
..



In the entity in the getter of a relationship I return a wrapper class


public class MyEntity{
 //..
 Collection customers = new HashSet();
 
public Collection getCustomers() {
        return new LazyCollectionWithReconnect

(customers, this, new MyProjectRelationshipInitializer());

    }



The implementation of the wrapper class is in the end of this post

The RelationShipInitializer

The interesting stuff is in MyProjectRelationshipInitializer
We have a generic interface:

public interface RelationshipInitializer {
    void initializeRelationship(Object entity, Object propertyValue);
}

That must be implemented by the project
Here I lookup a Stateless sessionbean that has access to the PersistenceContext and does the loading of the relationship

public class MyProjectRelationshipInitializer implements RelationshipInitializer{
    @Override
    public void initializeRelationship(Object entity, Object propertyValue) {
        MyGenericServiceLocator.inst().getGenericPersistenceService()
        .initializeRelationship(entity, propertyValue);
    }

}

The GenericPersistenceServiceBean that actually initializes the hibernate proxy

looks like this:

@Stateless
@Local(GenericPersistenceService.class)
public class GenericPersistenceServiceBean implements GenericPersistenceService
{

   @PersistenceContext
    protected EntityManager em;
 


@Override
    public void initializeRelationship(Object entity, Object relationShipObject) {
        org.hibernate.Session session = getHibernateSession();
        // the lock method adds the entity back into the Entitymanager makes it  

        //managed.
        try {
            session.lock(entity, LockMode.NONE);

            if(!Hibernate.isInitialized(relationShipObject))
            {
                Hibernate.initialize(relationShipObject);
            }

        } catch (TransientObjectException e) {
            //ignore object that are not saved yet
        }
    }


private Session getHibernateSession() {
        //on web as
        if (em.getDelegate() instanceof Session){
            return (Session) em.getDelegate();
        }
        //in openejb
        if (em.getDelegate() instanceof HibernateEntityManager){
            HibernateEntityManager e = (HibernateEntityManager) em.getDelegate();
            return (Session) e.getDelegate();
        }
        throw new IllegalStateException(String.format("Invalid type of entity manager %s (%s). Expected it to be org.hibernate.Session or org.hibernate.ejb.HibernateEntityManager", em, em.getClass()));
    }


}

Note that the getHibernateSession() is a little bit J2EE Server dependent and might be different for your vendor.

The LazyColletionWithReconnect implementation

 is rather boring:

public class LazyCollectionWithReconnect implements Collection {
   
    private Collection srcCollection;
    private Object surroundingEntity;
    private RelationShipInitializer relationShipInitializer;
   

    public LazyCollectionWithReconnect(Collection srcCollection, Object surroundingEntity, RelationShipInitializer relationShipInitializer) {
        super();
        this.srcCollection = srcCollection;
        this.surroundingEntity = surroundingEntity;
        this.relationShipInitializer = relationShipInitializer;
    }


  

     protected void reconnectIfNecessary() {
        ensureRelationIsLoaded(

           this.surroundingEntity, 
           this.srcCollection, 
           this.relationShipInitializer
        );
    }


          public static boolean ensureRelationIsLoaded(
       Object entity, 
       Object propertyValue, 
       RelationShipInitializer initializer) {
        boolean wasLoaded = false;
        //    first check
        if(!Hibernate.isInitialized(propertyValue)){
            initializer

            .initializeRelationship(entity, propertyValue);
            wasLoaded = true;
        }
        return wasLoaded;
    } 

    public boolean add(E e) {
        reconnectIfNecessary();
        return srcCollection.add(e);
    }
   public boolean addAll(Collection c) {
        reconnectIfNecessary();
        return srcCollection.addAll(c);
    }
    public void clear() {
        reconnectIfNecessary();
        srcCollection.clear();
    }
    public boolean contains(Object o) {
        reconnectIfNecessary();
        return srcCollection.contains(o);
    }
    public boolean containsAll(Collection c) {
        reconnectIfNecessary();
        return srcCollection.containsAll(c);
    }
    public boolean equals(Object o) {
        return srcCollection.equals(o);
    }
    public int hashCode() {
        return srcCollection.hashCode();
    }
    public boolean isEmpty() {
        reconnectIfNecessary();
        return srcCollection.isEmpty();
    }

    public Iterator iterator() {
        reconnectIfNecessary();
        return srcCollection.iterator();
    }
    public boolean remove(Object o) {
        reconnectIfNecessary();
        return srcCollection.remove(o);
    }

    public boolean removeAll(Collection c) {
        reconnectIfNecessary();
        return srcCollection.removeAll(c);
    }

    public boolean retainAll(Collection c) {
        reconnectIfNecessary();
        return srcCollection.retainAll(c);
    }

    public int size() {
        reconnectIfNecessary();
        return srcCollection.size();
    }


    public Object[] toArray() {
        reconnectIfNecessary();
        return srcCollection.toArray();
    }
    public
T[] toArray(T[] a) {
        reconnectIfNecessary();
        return srcCollection.toArray(a);
    }

}



Some common problems

HibernateException "reassociated object has dirty collection reference" 

This error message actually can be misleading. In my case it stemmed from that the entity with the collection did not have equals and hashCode overriden
 











No comments:

Post a Comment