Entity loads eagerly when it should be lazy


#1

Hi everyone,

I’m almost done with my migration project for Autofetch. I have run into some problems however regarding the prefetching of the test class entity. Autofetch is supposed to set all fetching to lazy before it does its calculations, so it can determine the optimal fetching strategy for each case during execution. The problem is that it seems like it does load the entity eagerly. This is the entity:

@Entity
@Tuplizer(impl = AutofetchTuplizer.class)
public class Employee {

    @Id
    @Column(name = "employee_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long m_id;

    @Column(name = "name")
    private String m_name;
    
    @JoinColumn(name = "supervisor_id")
    @ManyToOne(cascade = {CascadeType.ALL})
    private Employee m_supervisor;

    @CollectionType(type="org.autofetch.hibernate.AutofetchSetType")
    @JoinColumn(name = "supervisor_id")
    @OneToMany(cascade = {CascadeType.ALL})
    private Set<Employee> m_subordinates;

    @Embedded
    private Address m_address;

    @JoinColumn(name = "mentor_id")
    @ManyToOne(cascade = {CascadeType.ALL})
    private Employee m_mentor;

    @CollectionType(type="org.autofetch.hibernate.AutofetchSetType")
    @ManyToMany(cascade = {CascadeType.ALL})
    @JoinTable(name = "friends", joinColumns = {@JoinColumn(name = "friend_id")}, inverseJoinColumns = {@JoinColumn(name = "befriended_id")})
    private Set<Employee> m_friends;

This is the test that fails:

 @Test
    public void testLoadFetchProfile() {
        em.clearExtentInformation();

        // Execute query multiple times
        Employee dave = null;
        for (int i = 0; i < 2; i++) {
            dave = someAccess(i == 0);
        }

        // These all should not throw lazy instantiation exception, because
        // they should have been fetch eagerly
        dave.getSupervisor().getName();
        dave.getSupervisor().getSupervisor().getName();
        dave.getSupervisor().getSupervisor().getSubordinates().size();
        dave.getMentor().getName();

        // This should throw a lazy instantiation exception
        try {
            dave.getSupervisor().getSupervisor().getSupervisor().getName();
            // Shouldn't get here
            Assert.fail("Lazy instantion exception not thrown for a property which shouldn't have been fetched"); //Fails here
        } catch (LazyInitializationException e) {
            // Good
        }
    }

The things it loads in the test:

 private Employee someAccess(boolean traverse) {
        Session sess;
        Transaction tx = null;
        Long daveId = createObjectGraph(true);
        try {
            sess = openSession();
            tx = sess.beginTransaction();
            Employee dave = (Employee) sess.load(Employee.class, daveId);
            dave.getName();
            if (traverse) {
                dave.getSupervisor().getName();
                dave.getMentor().getName();
                dave.getSupervisor().getSupervisor().getName();
                dave.getSupervisor().getSupervisor().getSubordinates().size();
            }
            tx.commit();
            tx = null;
            return dave;
        } finally {
            if (tx != null) {
                tx.rollback();
            }
        }
    }

I have tried setting the fetching strategy manually for Supervisor to lazy, but that does not help either. Since I am quite new with Hibernate and mapping, I suspect I might have done some mistakes when changing the old xml mapping to annotation-based.

This is how the old file looked:

<?xml version="1.0"?>
-<!DOCTYPE hibernate-mapping PUBLIC 
-	"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
-	"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
-
-<hibernate-mapping>		
-	<class name="org.autofetch.test.Employee" table="employee">	
-		<id name="id" column="employee_id">
-			<generator class="native"/>
-		</id>
-	
-		<property name="name" length="255"/>
-		
-       	<set name="subordinates" inverse="true" cascade="all" >
-       		<key column="supervisor_id"/>
-       		<one-to-many class="org.autofetch.test.Employee"/>
-	   	</set>
-       
-       	<many-to-one name="supervisor" cascade="all" column="supervisor_id"/>
-         
-       	<many-to-one name="mentor" cascade="all" column="mentor_id"/>
-          
-       	<set name="friends" table="friends" cascade="all">
-       		<key column="friend_id"/>
-       		<many-to-many column="befriended_id" class="org.autofetch.test.Employee"/>
-       	</set>
-          
-       	<component name="address" class="org.autofetch.test.Address" lazy="false">        
-			<property name="street" length="255"/>
-       		<property name="city" length="255"/>
-			<property name="state" length="255"/>
-		</component>
-	</class>
-</hibernate-mapping>

The tool comes with its own LazyInitializer:

public class AutofetchLazyInitializer extends BasicLazyInitializer implements MethodHandler {

    private static final CoreMessageLogger LOG = CoreLogging.messageLogger(AutofetchLazyInitializer.class);

    private EntityTracker entityTracker;

    private boolean entityTrackersSet;

    private Class[] interfaces;

    private boolean constructed;

    private AutofetchLazyInitializer(String entityName,
                                     Class persistentClass,
                                     Class[] interfaces,
                                     Serializable id,
                                     Method getIdentifierMethod,
                                     Method setIdentifierMethod,
                                     CompositeType componentIdType,
                                     SessionImplementor session,
                                     Set<Property> persistentProperties,
                                     boolean classOverridesEquals) {

        super(entityName, persistentClass, id, getIdentifierMethod, setIdentifierMethod,
                componentIdType, session, classOverridesEquals);

        this.interfaces = interfaces;

        AutofetchService autofetchService = session.getFactory().getServiceRegistry().getService(AutofetchService.class);
        this.entityTracker = new EntityTracker(persistentProperties, autofetchService.getExtentManager());
        this.entityTrackersSet = false;
    }

    @Override
    public Object invoke(final Object proxy, final Method thisMethod, final Method proceed, final Object[] args) throws Throwable {
        if (this.constructed) {
            Object result;
            try {
                result = this.invoke(thisMethod, args, proxy);
            } catch (Throwable t) {
                throw new Exception(t.getCause());
            }

            if (result == INVOKE_IMPLEMENTATION) {
                if (args.length == 0) {
                    switch (thisMethod.getName()) {
                        case "enableTracking":
                            return handleEnableTracking();
                        case "disableTracking":
                            return handleDisableTracking();
                        case "isAccessed":
                            return entityTracker.isAccessed();
                    }
                } else if (args.length == 1) {
                    if (thisMethod.getName().equals("addTracker") && thisMethod.getParameterTypes()[0].equals(Statistics.class)) {
                        return handleAddTracked(args[0]);
                    } else if (thisMethod.getName().equals("addTrackers") && thisMethod.getParameterTypes()[0].equals(Set.class)) {
                        return handleAddTrackers(args[0]);
                    } else if (thisMethod.getName().equals("removeTracker") && thisMethod.getParameterTypes()[0].equals(Statistics.class)) {
                        entityTracker.removeTracker((Statistics) args[0]);
                        return handleRemoveTracker(args);
                    } else if (thisMethod.getName().equals("extendProfile") && thisMethod.getParameterTypes()[0].equals(Statistics.class)) {
                        return extendProfile(args);
                    }
                }

                final Object target = getImplementation();
                final Object returnValue;

                try {
                    if (ReflectHelper.isPublic(persistentClass, thisMethod)) {
                        if (!thisMethod.getDeclaringClass().isInstance(target)) {
                            throw new ClassCastException(
                                    target.getClass().getName() + " incompatible with " + thisMethod.getDeclaringClass().getName()
                            );
                        }
                    } else {
                        thisMethod.setAccessible(true);
                    }

                    returnValue = thisMethod.invoke(target, args);
                    if (returnValue == target) {
                        if (returnValue.getClass().isInstance(proxy)) {
                            return proxy;
                        } else {
                            LOG.narrowingProxy(returnValue.getClass());
                        }
                    }

                    return returnValue;
                } catch (InvocationTargetException ite) {
                    throw ite.getTargetException();
                } finally {
                    if (!entityTrackersSet && target instanceof Trackable) {
                        entityTrackersSet = true;
                        Trackable entity = (Trackable) target;
                        entity.addTrackers(entityTracker.getTrackers());
                        if (entityTracker.isTracking()) {
                            entity.enableTracking();
                        } else {
                            entity.disableTracking();
                        }
                    }
                }
            } else {
                return result;
            }
        } else {
            // while constructor is running
            if (thisMethod.getName().equals("getHibernateLazyInitializer")) {
                return this;
            } else {
                return proceed.invoke(proxy, args);
            }
        }
    }

    private Object handleDisableTracking() {
        boolean oldValue = entityTracker.isTracking();
        this.entityTracker.setTracking(false);
        if (!isUninitialized()) {
            Object o = getImplementation();
            if (o instanceof Trackable) {
                Trackable entity = (Trackable) o;
                entity.disableTracking();
            }
        }

        return oldValue;
    }

    private Object handleEnableTracking() {
        boolean oldValue = this.entityTracker.isTracking();
        this.entityTracker.setTracking(true);

        if (!isUninitialized()) {
            Object o = getImplementation();
            if (o instanceof Trackable) {
                Trackable entity = (Trackable) o;
                entity.enableTracking();
            }
        }

        return oldValue;
    }

    private Object extendProfile(Object[] params) {
        if (!isUninitialized()) {
            Object o = getImplementation();
            if (o instanceof TrackableEntity) {
                TrackableEntity entity = (TrackableEntity) o;
                entity.extendProfile((Statistics) params[0]);
            }
        } else {
            throw new IllegalStateException("Can't call extendProfile on unloaded self.");
        }

        return null;
    }

    private Object handleRemoveTracker(Object[] params) {
        if (!isUninitialized()) {
            Object o = getImplementation();
            if (o instanceof Trackable) {
                Trackable entity = (Trackable) o;
                entity.removeTracker((Statistics) params[0]);
            }
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    private Object handleAddTrackers(Object param) {
        Set<Statistics> newTrackers = (Set<Statistics>) param;
        this.entityTracker.addTrackers(newTrackers);
        if (!isUninitialized()) {
            Object o = getImplementation();
            if (o instanceof Trackable) {
                Trackable entity = (Trackable) o;
                entity.addTrackers(newTrackers);
            }
        }

        return null;
    }

    private Object handleAddTracked(Object param) {
        this.entityTracker.addTracker((Statistics) param);
        if (!isUninitialized()) {
            Object o = getImplementation();
            if (o instanceof Trackable) {
                Trackable entity = (Trackable) o;
                entity.addTracker((Statistics) param);
            }
        }

        return null;
    }

    @Override
    protected Object serializableProxy() {
        return new AutofetchSerializableProxy(
                getEntityName(),
                this.persistentClass,
                this.interfaces,
                getIdentifier(),
                (isReadOnlySettingAvailable() ? Boolean.valueOf(isReadOnly()) : isReadOnlyBeforeAttachedToSession()),
                this.getIdentifierMethod,
                this.setIdentifierMethod,
                this.componentIdType,
                this.entityTracker.getPersistentProperties()
        );
    }

    public static HibernateProxy getProxy(
            final String entityName,
            final Class persistentClass,
            final Class[] interfaces,
            final Method getIdentifierMethod,
            final Method setIdentifierMethod,
            final CompositeType componentIdType,
            final Serializable id,
            final SessionImplementor session,
            final Set<Property> persistentProperties) throws HibernateException {

        // note: interface is assumed to already contain HibernateProxy.class
        try {
            final AutofetchLazyInitializer instance = new AutofetchLazyInitializer(
                    entityName,
                    persistentClass,
                    interfaces,
                    id,
                    getIdentifierMethod,
                    setIdentifierMethod,
                    componentIdType,
                    session,
                    persistentProperties,
                    ReflectHelper.overridesEquals(persistentClass)
            );

            final ProxyFactory factory = new ProxyFactory();
            factory.setSuperclass(interfaces.length == 1 ? persistentClass : null);
            factory.setInterfaces(interfaces);
            factory.setFilter(FINALIZE_FILTER);
            Class cl = factory.createClass();
            final HibernateProxy proxy = (HibernateProxy) cl.newInstance();
            ((Proxy) proxy).setHandler(instance);
            instance.constructed = true;
            return proxy;
        } catch (Throwable t) {
            LOG.error(LOG.javassistEnhancementFailed(entityName), t);
            throw new HibernateException(LOG.javassistEnhancementFailed(entityName), t);
        }
    }

    public static HibernateProxy getProxy(
            final Class factory,
            final String entityName,
            final Class persistentClass,
            final Class[] interfaces,
            final Method getIdentifierMethod,
            final Method setIdentifierMethod,
            final CompositeType componentIdType,
            final Serializable id,
            final SessionImplementor session,
            final Set<Property> persistentProperties) throws HibernateException {

        // note: interfaces is assumed to already contain HibernateProxy.class
        final AutofetchLazyInitializer instance = new AutofetchLazyInitializer(
                entityName,
                persistentClass,
                interfaces,
                id,
                getIdentifierMethod,
                setIdentifierMethod,
                componentIdType,
                session,
                persistentProperties,
                ReflectHelper.overridesEquals(persistentClass)
        );

        final HibernateProxy proxy;
        try {
            proxy = (HibernateProxy) factory.newInstance();
        } catch (Exception e) {
            throw new HibernateException("Javassist Enhancement failed: " + persistentClass.getName(), e);
        }

        ((Proxy) proxy).setHandler(instance);
        instance.constructed = true;

        return proxy;
    }

    private static final MethodFilter FINALIZE_FILTER = new MethodFilter() {

        @Override
        public boolean isHandled(Method m) {
            // skip finalize methods
            return !(m.getParameterTypes().length == 0 && m.getName().equals("finalize"));
        }
    };
}

Does anyone have an idea why it has this behaviour? Thanks in advance.


#2

By default, @ManyToOne and @ObeToOne are EAGER. You need to set those to LAZY. And also, parent side one-to-one are also EAGER, unless you use bytecode enhancement.


#3

Thansks for the reply. I read this aswell, and I tried setting it manually for the supervisor. It didn’t change the outcome of the tests however. Do you know if there is a way to make hibernate to always lazy fetch all types?

Update: I checked now an older post that you did on your website and I saw the possibility to use the bytecode enhancement that you mentioned. Is this a good way to always load lazily? Can you still apply hints to individual queries to for example load something eagerly?


#4

Do you know if there is a way to make hibernate to always lazy fetch all types?

As long as you set associations to LAZY, Hibernate should honor that. The mappedBy one-to-one is the exception to the rule. But you can use bytecode enhancement to address that too.

Is this a good way to always load lazily?

Yes.

Can you still apply hints to individual queries to for example load something eagerly?

Yes. You can use JOIN FETCH.