Carga Perezosa en Clientes Pesados.


Introducción


Hibernate es un mapeador de objetos java a base de datos (ORM) que se distribuye mediante licencia LGPL. Una optimización común y necesaria en los ORMs es la carga perezosa (Lazy Load) que consiste en realizar una carga parcial del objeto, posponiendo la carga de algunos de sus componentes hasta que realmente el programa acceda a ellos. El problema que nos ocupa ocurre cuando se accede a un miembro de un objeto con carga perezosa cuando este se encuentra "desconectado" (detached) de la base de datos y por lo tanto ya no es capaz de recuperar los datos de esta. Hibernate trata esta situación como un error de programación y lanza la famosa excepción de carga perezosa, LazyInitializationException.

Existen distintas estrategias para abordar el problema de las excepciones de carga perezosa dependiendo del entorno de desarrollo. En entornos web, por ejemplo, la solución mas aceptada es Open Session In View (OSIV) que consiste en dejar la sesión abierta, en modo sólo lectura hasta que se realiza el renderizado de la página web. Sin embargo, en una aplicación Swing, RCP o RIA por ejemplo, OSIV no suele ser viable ya que implicaría tener una sessión abierta durante demasiado tiempo, si no durante toda la vida de la aplicación.

Este artículo trata de las distintas soluciones que pueden adoptarse para resolver los problemas derivados de la carga perezosa en clientes pesados o RIA en los que no se puede utilizar Open Session In View.


Carga Perezosa e Integridad


El principal problema que plantea la carga perezosa es que la carga inicial de los datos y las cargas posteriores ocasionadas por los accesos a miembros perezosos deben realizarse en el marco de una única sesión o unidad de trabajo (Unit of Work). De no ser así, cabe la posibilidad de que falle la integridad de los datos que el modelo representa. Veamos esto en más detalle con un ejemplo.

Una aplicación de gestión de proyectos mantiene la siguiente relación para registrar las horas que los trabajadores dedican a cada proyecto en los que están asignados. Las clases que modelan los trabajadores y los proyectos contienen colecciones que representan la relación M:N que existe entre trabajadores y proyectos. de este modo, el método getUsers() de la clase Project devuelve la lista de trabajadores que han apuntado horas a un proyecto y el método getProjects() de la clase Users devuelve una colección de los proyectos en los que un trabajador ha cargado horas.

Si mapeamos está relación con un ORM sin que sea perezosa, tendremos el problema que al cargar un usuario de la base de datos nos traeremos también todos los usuarios con todos los proyectos en los que han trabajado. Esto es demasiado esfuerzo si por ejemplo, sólo vamos a listar los usuarios en una tabla.

Los métodos principales que suelen utilizarse en este caso son:

En mi opinión, la carga especializada para cada caso de uso es la más factible en la mayoría de los casos. Sin embargo la carga bajo demanda del objeto, puede automatizarse mediante AOP (como veremos a continuación), simplificando considerablemente el desarrollo una vez que se tienen en cuenta sus limitaciones.


Carga Parcial Automática mediante AOP


La carga parcial del objeto User puede hacerse de forma muy similar al patrón OSIV en entornos web, desde el punto de vista del desarrollo del cliente si se utiliza la programación orientada a aspectos para detectar el acceso a la colección no inicializada de Proyectos y cargar esta bajo demanda (abriendo una sesión en modo solo lectura) justo antes de que Hibernate lance la excepción LazyInicializationException evitando así la programación del método findProjectsByUser. Es importante notar que esta similitud se encuentra únicamente en la programación del código del cliente ya que las consecuencias son bastante distintas:

Como consecuencia, cargar las colecciones perezosas arrastra dos problemas bastante serios:

Lejos de ser una solución tan ventajosa como puede parecer en un principio, no es en mi opinión, tan mala como para no tenerla en cuenta debido principalmente a la considerable simplificación que produce en el código de la aplicación. Sobre todo si se tiene en cuenta que el aspecto no inicializará colecciones ya inicializadas por lo que puede utilizarse como complemento de la carga especializada para cada caso de uso en aquellos casos en los que la pérdida de aislamiento o de rendimiento no sean importantes y se quiera ganar en generalidad y/o simplicidad.

Como ventaja adicional, El aspecto subsanará los errores de falta de inicialización en tiempo de producción evitando el fallo de la aplicación, dando lugar, en muchas ocasiones, al mismo resultado que si el error de inicialización en el DAO no se hubiera producido.


HibernateLazyGuard


El aspecto pertenece a jdal-aspects, un módulo de la librería jdal (www.jdal.org) introducido en la versión de desarrollo 1.1-SNAPSHOT.

Para determinar si la colección está inicializada, introducimos el método público isDetached() en AbstractCollection y controlamos el acceso a la bandera AbstracCollection.initialized. Suponemos que si el código en ejecución está comprobando el valor de AbtractCollection.initialized cuando vale false, la colección está desconectada de la sesión y no está activa la gestión de transacciones de Spring, es el momento de inicializarla.

/*
 * Copyright 2009-2011 original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jdal.aspects;
 
import java.util.Collection;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.FlushMode;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.collection.AbstractPersistentCollection;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.impl.SessionImpl;
import org.hibernate.persister.collection.CollectionPersister;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
 
/**
 * Hibernate Guard for LazyInitializationException. Open read only session and 
 * load Uninitialized proxy o collection from database before access to it.
 * 
 * @author Jose Luis Martin - (jlm@joseluismartin.info)
 */
privileged public aspect HibernateLazyGuard {
 
	/** hibernate session factory used to create sessions */
	private SessionFactory sessionFactory;
	/** common logging log */
	private static final Log log = LogFactory.getLog(HibernateLazyGuard.class);
 
	/**
	 * Test if PersistentCollection is connected to session
	 * @return true if not connected (detached)
	 */
	public boolean  AbstractPersistentCollection.isDetached() {
		return  !(isConnectedToSession() && session.isConnected());
	}
 
    /** match Collection public methods calls */
	pointcut collection() : call (public * Collection.*(..)) && !within(HibernateLazyGuard);
 
	/** match join points on this advice */
	pointcut me() : cflow(adviceexecution()) && within(HibernateLazyGuard);
 
	/** match access to intialized flag on APC, only if call is in flow of woven collection method */
	pointcut initialized(AbstractPersistentCollection apc) : 
		get(boolean org.hibernate.collection.AbstractPersistentCollection.initialized) && 
		this(apc);
 
	/**
	 * Before Advice, if Collection is unitialized open a new Session
	 * to initialize Collection
	 * @param c AbstractPersistentCollection 
	 */
	boolean around(AbstractPersistentCollection apc) : initialized(apc) && !cflow(me())   {
		if (log.isDebugEnabled())
			log.debug(thisJoinPointStaticPart.toString());
 
		if (!SessionFactoryUtils.hasTransactionalSession(sessionFactory) &&
				!apc.wasInitialized() && apc.isDetached()) {
			log.warn("PersistentCollection will throw exception: " + apc.getRole());
			Session session = sessionFactory.openSession();
			session.setFlushMode(FlushMode.MANUAL);
			Transaction tx = null;
			try {
				tx = session.beginTransaction();
				attachToSession(apc, session);
				Hibernate.initialize(apc);  // will throw lazy if attach was failed
				tx.commit();
			} 
			catch (RuntimeException e) {
				tx.rollback();	
				throw e;
			}
			finally {
				session.close();
			}
		}
		// return true
		return proceed(apc);
	}
 
 
	/**
	 * If PersistentCollection is uninitialized, attach it to new session and 
	 * initialize it. 
	 * @param ps persistent collection
	 * @param session hibernate sesion to use for initialization
	 */
	@SuppressWarnings("unchecked")
	public void attachToSession(AbstractPersistentCollection ps, Session session) {
		if (log.isDebugEnabled())
			log.debug("Attatching PersistentCollection of role: " + ps.getRole());
 
		if (!ps.wasInitialized()) {
			SessionImpl source = (SessionImpl) session;
			PersistenceContext context = source.getPersistenceContext();
			CollectionPersister cp = source.getFactory().getCollectionPersister(ps.getRole());
 
			if (!context.containsCollection(ps)) {  // detached
				context.addUninitializedDetachedCollection(cp, ps);
			}
 
			ps.setCurrentSession(context.getSession());
		}
	}
 
	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}
 
 
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}
}
 

Aplicación de ejemplo


El ejemplo consiste en una relación OneToMany entre las clases Perro y Gato. Supongamos que cada gato odia a un perro, por ejemplo. La aplicación simplemente obtiene un perro de la BD y lista los gatos que le odian una vez que la sessión se ha cerrado. Puede descargar el ejemplo completo de jdal-aspects-sample.tgz.

Para aplicar el aspecto con weaving en tiempo de ejecución (Load Time Weaving, LTW) es necesario configurar el contexto de spring con <context:load-time-weaver/> y añadir al comando java la opción -javaagent:lib/spring-instrumentation-3.0.5.RELASE.jar.

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"
		default-lazy-init="false" default-init-method="init">
 
	<bean id="propertyConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<list>
				<value>classpath*:/jdbc.properties</value>
			</list>
		</property>
	</bean>
 
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
		<property name="driverClass" value="${jdbc.driverClassName}" />
		<property name="jdbcUrl" value="${jdbc.url}" />
		<property name="user" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
 
		<property name="acquireIncrement" value="3" />
		<property name="minPoolSize" value="2" />
		<property name="maxPoolSize" value="10" />
		<property name="maxIdleTime" value="5" />
		<property name="numHelperThreads" value="5" />
 
		<property name="idleConnectionTestPeriod" value="10" />
		<property name="autoCommitOnClose" value="false" />
		<property name="preferredTestQuery" value="select 1;" />
		<property name="testConnectionOnCheckin" value="true" />
 
		<property name="checkoutTimeout" value="60000" />
	</bean>
 
	<!-- Hibernate SessionFactory -->
    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:hibernate.cfg.xml"/>
    </bean>
 
	 <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
 
	<bean id="catDao" class="info.joseluismartin.dao.hibernate.HibernateDao">
		<constructor-arg value="org.jdal.aspects.sample.Cat"/>
		<property name="sessionFactory" ref="sessionFactory"/>
	</bean>
 
	<bean id="dogDao" class="info.joseluismartin.dao.hibernate.HibernateDao">
		<constructor-arg value="org.jdal.aspects.sample.Dog"/>
		<property name="sessionFactory" ref="sessionFactory"/>
	</bean> 
 
 
	<!-- Tx Advice -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!-- the transactional semantics... -->
    <tx:attributes>
      <!-- all methods starting with 'get' and 'load' are read-only -->
      <tx:method name="get*" read-only="true"/>
      <tx:method name="load*" read-only="true"/>
      <!-- other methods use the default transaction settings -->
      <tx:method name="*"/>
    </tx:attributes>
  </tx:advice>
 
	 <aop:config>
       <!-- Make all methods on package service transactional  -->
       <aop:pointcut id="daoOperation" expression="execution(* info.joseluismartin.dao.*.*(..))"/>
       <aop:advisor advice-ref="txAdvice" pointcut-ref="daoOperation" order="1"/>
	</aop:config>
 
	<context:load-time-weaver/>
 
	<bean id="lazyGuard" class="org.jdal.aspects.HibernateLazyGuard" factory-method="aspectOf">
		<property name="sessionFactory" ref="sessionFactory"/>
	</bean>
 
</beans>
 

finalmente, la clase Main que contiene el caso de prueba

/*
 * Copyright 2009-2011 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jdal.aspects.sample;
 
import info.joseluismartin.beans.AppCtx;
import info.joseluismartin.dao.Dao;
 
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
/**
 * @author Jose Luis Martin - (jlm@joseluismartin.info)
 *
 */
public class Main {
 
	private static final Log log = LogFactory.getLog(Main.class);
 
	@SuppressWarnings("unchecked")
	public static void main (String[] arg) {
		AppCtx.getInstance();
		Dao<Dog, Long> dogDao = (Dao<Dog, Long>) AppCtx.getInstance().getBean("dogDao");
		Dog dog = dogDao.get(1l);
		dogDao.delete(dog);
 
		int i = 0;
 
		for (Cat cat : dog.getCats()) 
			log.info(cat.getName());
	}
 
}
 

Para ejecutar el ejemplo, cree la base de datos con el script en db/create.sql, edite el fichero jdbc.properties y compílelo con el comando mvn compile. Finalmente puede ejecutarlo con el comando mvn exec:exec.