Lazy Fetch en JPA

Una de las características que ofrece JPA (entre otros frameworks de mapeo relacional de objetos) que más puede beneficiarnos cuando la entendemos y aplicamos de forma correcta, y que más quebraderos de cabeza provoca a quienes están empezando, es Lazy Fetch. Aunque ya hemos hablado con anterioridad de forma superficial sobre Lazy Fetch durante el tutorial de JPA publicado en esta misma web, debido a la cantidad de personas que se ponen en contacto conmigo al respecto y por la satisfacción inherente de entender un poco mejor una importante caracteristica del lenguaje, he considerado interesante escribir un pequeño artículo donde se analice con cierta profundidad qué es, cómo funciona, y cómo podemos beneficiarnos (además de cómo evitar errores típicos) de Lazy Fetch . Comencemos.

1. Lazy Fetch A.K.A. lectura demorada

El termino Lazy Fetch, que traducido literalmente significa obtención perezosa (aunque yo prefiero entenderlo como lectura demorada), viene a reflejar la característica de JPA que permite que, cuando existen relaciones entre distintas entidades, las que son dependientes de otras no sean inicializadas con sus valores almacenados en base de datos hasta que no sean explícitamente accedidas (leidas). Toda esta verborrea se entiende mejor con un sencillo ejemplo:

@Entity 
public class Factura { 
    // ... 
} 

@Entity 
public class Proveedor { 
    // ... 

    @OneToMany 
    private List facturas; 
} 
	

En el ejemplo anterior, tenemos una entidad contenedora (Proveedor) que contiene una relación uno-a-muchos (@OneToMany) con otra entidad, Facturas a través de un objeto List. Hasta aquí todo es bastante simple, por lo que... ¡compliquémoslo!

Imaginemos que cada proveedor concreto suele tener multitud de facturas asociadas (algo bastante habitual), aunque gran parte de las consultas a la entidad Proveedor no están accediendo en ningún momento a dichas facturas; cada obtención de un proveedor desde base de datos supondría realizar diferentes subconsultas para inicializar todas sus facturas, de manera que estaríamos incurriendo en una sobrecarga innecesaria de los recursos del sistema (esto en un ejemplo de juguete como el nuestro no es apreciable ni notorio, pero en un sistema real que esté recibiendo miles o decenas de miles de peticiones por minuto (por poner una cifra) puede sobrecargar y hasta agotar los recursos de la red, procesadores, ram, base de datos, etc...).

Por este motivo, JPA permite marcar una relación entre entidades como Lazy, de manera que la inicialización desde base de datos de los objetos afectados es retrasada hasta que éstos son explicitamente requeridos por código cliente.

2. ¿Quién es Lazy?

Por defecto, de los cuatro tipos de relación entre entidades permitidos en JPA (@OneToOne, @OneToMany, @ManyToOne y @ManyToMany) sólo dos son de tipo Lazy: @OneToMany y @ManyToMany. Esto tiene mucho sentido, pues son estos dos tipos los que a más objetos conectan en el otro lado de la relación. Al ser el comportamiento por defecto, no debemos hacer nada para declarar estas relaciones como Lazy (es su comportamiento implícito).

El resto de relaciones no-Lazy (@ManyToOne y @OneToOne) deben ser marcadas explícitamente como Lazy si queremos que se comporten de dicha manera:

@Entity 
public class Empleado { 
    // ... 

    @OneToOne(fetch=FetchType.LAZY) 
    private Direccion direccion; 
} 
	

3. El gran problema

Una vez una entidad contenedora ha sido desconectada (dettached) del gestor de persistencia (por ejemplo al enviarla de vuelta al código cliente que la solicitó), esta se enviará tal como esté en ese momento sin importar en que estado estén sus relaciones que hayan sido marcadas como Lazy. Si una relación ha sido inicializada antes de desconectar la entidad del gestor de persistencia, podremos acceder a sus valores de forma normal; en caso contrario, la relación no apuntará a ningún objeto, y por tanto obtendremos un error al intentar manejarla:

@Stateless 
public class ClaseDelLadoServidor { 
    // ... 
     
    public Proveedor obtenerProveedor(Long proveedorId) { 
        // Conectamos con BBDD y obtenemos la entidad a devolver 
         
        return proveedor;        // Supongamos que en este momento la colección facturas 
                                // no ha sido inicializada aún 
    } 
} 

public class ClaseDelLadoCliente { 
    public void procesarFacturas(long proveedorId) { 
        Proveedor proveedor = claseLadoServidor.obtenerProveedor(proveedorId); 
        for(Factura factura : proveedor.getFacturas()) { 
            // Procesar cada factura 
        } 
    } 
}
	

En el ejemplo anterior, la simple llamada a getFacturas() en la clase del lado del cliente provocaría una excepción que, dependiendo de tu proveedor de persistencia (en mi caso es EclipseLink) se parecerá más o menos a esto:

Exception in thread "main" Local Exception Stack: 
Exception [EclipseLink-7242] (Eclipse Persistence Services - 2.0.1.v20100213-r6600): org.eclipse.persistence.exceptions.ValidationException 
Exception Description: An attempt was made to traverse a relationship using indirection that had a null Session. This often occurs when an entity with an uninstantiated LAZY relationship is serialized and that lazy relationship is traversed after serialization. To avoid this issue, instantiate the LAZY relationship prior to serialization. 
    at org.eclipse.persistence.exceptions.ValidationException.instantiatingValueholderWithNullSession(ValidationException.java:979)
    at org.eclipse.persistence.internal.indirection.UnitOfWorkValueHolder.instantiate(UnitOfWorkValueHolder.java:219) 
    at org.eclipse.persistence.internal.indirection.DatabaseValueHolder.getValue(DatabaseValueHolder.java:83) 
    at org.eclipse.persistence.indirection.IndirectList.buildDelegate(IndirectList.java:237) 
    at org.eclipse.persistence.indirection.IndirectList.getDelegate(IndirectList.java:397) 
    at org.eclipse.persistence.indirection.IndirectList.size(IndirectList.java:726) 
    at es.davidmarco.ejb.lazyloading.standaloneclient.LazyLoadingClient.main(LazyLoadingClient.java:43) 
	

El mensaje principal que se informa en el error es el siguiente:

Se ha realizado un intento de recorrer una relación mediante indirección que tenía una sesión null. Esto ocurre a menudo cuando una entidad que contiene una relación Lazy sin inicializar es serializada y dicha relación es recorrida después de la serialización. Para evitar este problema, inicializa la relación Lazy de forma previa a la serialización.

Los primeros de la clase se estarán preguntando: ¿cómo es posible que nuestro cliente, que posiblemente se esté ejecutando fuera de un contenedor de aplicaciones, tal vez incluso en otra máquina funcionando bajo una JVM distinta, esté lanzando una excepción JPA en lugar de algo como la archimalvada NullPointerException? El proveedor de persistencia puede hacer esto de algunas maneras, como devolviendo objetos proxy que envuelven al objeto original, o manipulando el código de bytes, entre otras. No voy a profundizar en este asunto pues no nos afecta de forma directa, pero tenlo simple presente de manera que, cuando suceda, sepas lo que está ocurriendo.

4. La gran solución

Es un poco atrevido pensar que existe una gran solución a nuestro gran problema, pues primero deberíamos preguntarnos si éste último es realmente tal; hasta ahora lo hemos llamado así por el simple hecho de que a todos los que hemos trabajado con JPA nos ha ocurrido en alguna ocasión, sobre todo al principio, y los sentimientos que produce van desde singular hasta desconcertante (sobre todo si no sabemos que está ocurriendo y, todavía peor, tampoco porqué). Lo cierto es que no es ningún problema, y tal como hemos dicho anteriormente, Lazy Fetch trata simple y llanamente de eso: enviar la información necesaria, y omitir el resto hasta que sea explicitamente accedida.

Una solución para evitar enviar a un cliente relaciones no inicializadas es definirlas explícitamente como no-lazy (o dicho de forma más correcta, Eager):

@Entity 
public class Empleado { 
    // ... 

    @ManyToMany(fetch=FetchType.EAGER) 
    private List proyectos; 
} 
	

También podemos inicializar implícitamente las entidades asociadas a través de algún mecanismo del lenguaje:

@Stateless 
public class ClaseDelLadoServidor { 
    // ... 
     
    public Proveedor obtenerProveedor(Long proveedorId) { 
        // Conectamos con BBDD y obtenemos la entidad a devolver 
         
        facturas.size();        // Inicializa la relación al completo. No usar para inicializar de 
                                // forma explícita, en su lugar usar fetch=FetchType.EAGER. 
         
        return proveedor; 
    } 
}
	

Para no violar el cometido principal de Lazy, sólo debemos inicializar una relación (ya sea de forma explícita o implícita) en escenarios como los siguientes:

  • Cuando conocemos de antemano que los clientes de nuestra entidad van a acceder a el/los miembros de la relación después de que esta sea desconectada del contexto de persistencia.
  • Cuando, a pesar de mantenernos aún dentro de una transacción, sabemos que se van a producir multitud de llamadas a los elementos referenciados en la relación (cada una de ellas realizando sus peticiones a base de datos de forma exclusiva e independiente de las demas), de manera que el coste total va a terminar siendo mayor que si hubiéramos inicializado la relación en el momento de la inicializar la entidad contenedora.

Evidentemente, estas situaciones no son faciles de preveer en la mayoría de los casos, más aún cuando no tenemos control sobre el código cliente. Éste último, si está bajo nuestro control, no debe hacer llamadas a relaciones no inicializadas (evidentemente). En caso de no conocer de antemano si una relación ha sido inicializada, debemos manejar un posible error mediante tratamiento de excepciones (esto es, capturando la excepción e inicializando la relación dentro del bloque catch). Al margen del tratamiento de excepciones, si deseamos acceder a una relación que sabemos que no está inicializada, deberemos gestionarlo a través de lógica de negocio adicional (podemos enviar la entidad contenedora de vuelta al servidor de aplicaciones para que sea refrescada con la información ausente y devuelta de nuevo al cliente, solicitar sólo las entidades contenidas en la relación a traves de algún valor de la entidad contenedora que permita identificarlas, etc...). Esta lógica adicional estará integrada en el lado del servidor, pues es allí donde tenemos acceso al contexto de persistencia.

5. Mojándonos los pies

Es importante entender que ocurre entre bambalinas cuando tratamos con relaciones de tipo Lazy, por lo que vamos a sumergirnos un poco (aunque muy poco) en la forma en que esto ocurre dentro de JPA. Todas (repito, todas) las operaciones de persistencia en JPA se producen dentro de una transacción. Tarde o temprano, toda transacción finaliza, y en ese momento todas las entidades involucradas son desconectadas del contexto de persistencia, dejando de estar gestionadas por EntityManager.

En el ejemplo de la sección anterior, la clase ClaseDelLadoServidor ha sido marcada como @Stateless, de manera que ya sea mediante persistencia manejada por el contenedor (JTA) o controlada manualmente (RESOURCE-LOCAL), en el momento de terminar la ejecución del método se debe dar por terminada la transacción. Es en este momento cuando la entidad puede ser devuelta al cliente que la solicitó.

En el caso de estar trabajando con componentes de tipo @Statefull, podemos mantener una transacción por un periodo de tiempo mayor al de una única invocación a cualquiera de los métodos del componente, por lo que la entidad y sus relaciones pueden permanecer gestionadas por un tiempo mayor al de una única invocación a cualquiera de los métodos del componente (esto implica que cualquier acceso a una relación no inicializada se trataría de forma transparente para nosotros).

Al trabajar con relaciones Lazy, debemos usar siempre interfaces (como List o Map) en lugar de implementaciones concretas (como ArrayList o HashMap): esto, además de ser una las grandes-reglas/buenas-prácticas cuando utilizamos un lenguaje orientado a objetos como Java, va a permitir al proveedor de persistencia usar sus propias implementaciones lo que a su vez nos permitirá a nosotros beneficiarnos de la magia de Lazy Fetch.

6. La otra cara de la moneda

El comportamiento opuesto a Lazy Fetch, como vimos en un ejemplo anterior, es Eager Fetch (el término puede traducirse literalmente como obtención impaciente, pero yo, de nuevo, prefiero entenderlo como inicialización temprana). Cuando una relación es de tipo Eager, o es marcada con este comportamiento de forma explícita, ésta es inicializada en el mismo instante en que lo es su entidad contenedora. Esto conlleva cero problemas para el código cliente, pues a efectos prácticos estamos en la misma situación que cuando trabajamos con POJOs en un entorno de no-persistencia (se aplican las reglas de construcción típicas del class loader). Tan simple como eso.

7. Uso y abuso

Lazy Fetch tiene un cometido concreto, y esto por si sólo ya es motivo para su existencia. El coste de crear objetos puede ser inmenso si todas las relaciones dentro de una entidad (relaciones que a su vez pueden contener sus propias relaciones, y así indefinidamente) fueran iniciadas en cascada desde el momento de creación de la entidad contenedora original. Otra situación en la cual Lazy Fetch es sumamente útil es cuando definimos una relación con objetos de gran tamaño (como BLOBs y CLOBs). Esta clase de relaciones suelen definirse como Lazy y, en caso de ser necesario el/los objeto/s referenciados, accedidos mediante lógica adicional. Todo esto representa el buen uso de Lazy Fetch.

Por supuesto podemos inicializar explícitamente todas las relaciones como Eager y olvidarnos de todo lo demás, y esto representa el mal uso de la arquitectura. Tarde o temprano terminaremos incurriendo en costes extra, ya que entre todos los objetos que estaremos inicializando, multitud de ellos serán accedidos en muy raras ocasiones (o incluso ninguna).

En este punto, y a pesar del gran problema que tuvimos unas secciones más atrás, podríamos pensar que "Lazy Fetch es un mecanismo mejor que Eager Fetch"; nada más lejos de la realidad, pues podemos terminar cometiendo un pecado distinto pero igual de grave, el de marcar toda relación viviente como Lazy: puesto que ninguna relación estará inicializada dentro de la entidad contenedora, todos y cada una de los accesos que realicemos sobre ellas conllevará el coste extra de una nueva petición a base de datos, situándonos de nuevo frente a posibles futuros problemas de agotamiento de recursos.

La solución, como casi siempre, se encuentra en encontrar el equilibrio (en nuestro caso determinar que componentes deben ser Lazy y cuales Eager). Si bien esto es algo que no puede ser definido con exactitud en el momento de iniciar el desarrollo de una aplicación, lo será en mayor o menor medida durante su periodo de vida (en parte gracias a herramientas externas y en parte gracias al propio comportamiento y/o respuesta de la aplicación ante situaciones de estress).

Resumen

Esto es todo lo que necesitas saber para, si no ser el gran Gurú de Lazy Fetch, si al menos saber en cierto detalle qué es y cómo funciona. Puedes encontrar información adicional en esta misma web sobre persistencia en general y JPA en particular aquí y aquí.