Hibernate Fetch y Lazy Loading

Hibernate (http://www.hibernate.org/) es una potente herramienta de persistencia que nos permite mapear clases en una base de datos relacional.

En este tutorial vamos a ver como se comportan ciertas relaciones, y como podemos optimizar las consultas a la base de datos. No pretende ser una guía detallada, sino una introducción a las características de Hibernate en este sentido.


Esta fuera del propósito de este tutorial el especificar la sintaxis para mapear las relaciones entre clases a la base de datos relacional.

Se recomienda al lector leer detalladamente la documentación de Hibernate al respecto. En especial el capítulo 19.1. Fetching strategies.

2. Entorno

El tutorial está escrito usando el siguiente entorno:
  • Hardware: Portátil Ahtec Signal 259M MXM (Sonoma 2.1 GHz, 2048 MB RAM, 100 GB HD).
  • Sistema Operativo: GNU / Linux, Debian Sid (unstable), Kernel 2.6.16, KDE 3.5
  • Máquina Virtual Java: JDK 1.5.0_06 de Sun Microsystems
  • Eclipse 3.1.2
  • Hibernate 3.1.3
  • Hibernate Annotations 3.1 beta9
  • Hibernate Tools 3.1.0 beta4

3. Colecciones

Por ejemplo cuando tenemos una relación de uno a muchos entre dos clases persistentes A y B, significa que en la clase A tendremos una colección de objetos de la clase B.

En la base de datos esta relación se establece con dos tablas TA y TB, donde en TB hay un campo que es la clave ajena de la tabla TA.

------------+          +-----------+
| TA        | -------> | TB        |
+-----------+  1    n  +-----------+
| idTA (PK) |          | idTB (PK) |
+-----------+          | idTA (FK) |
                       +-----------+
PK = Clave primaria
FK = Clave ajena

Un ejemplo de mapeo para esta relación podría ser:

<hibernate-mapping>
    <class name="A" table="TA">
         
        <id column="idTA" name="idTA">
            <generator class="native"/>
        </id>
  
        
        <set name="bs">
            <key column="idTA" />
            <one-to-many class="B" />
        </set>
     </class>
</hibernate-mapping>


<hibernate-mapping>
    <class name="B" table="TB">
        
        <id column="idTB" name="idTB">
            <generator class="native"/>
        
</class> </hibernate-mapping>

Por defecto en Hibernate 3.1, cuando intentemos recuperar los objetos de A se hará un consulta a la base de datos para recuperar los datos de la tabla TA, pero no se traerá los datos correspondientes de la tabla TB. En su lugar Hibernate proporciona un proxy. Este proxy se encarga de hacer las consultas a la base de datos cuando realmente se consulta la colección de objetos de la clase B.

Es decir, según este esquema, para recuperar los datos de TA y sus objetos relacionados de TB se necesitarían 1+n consultas a la base de datos, donde n es el número de objetos de A (filas en TA).

En muchos casos esta aproximación es adecuada, ya que no se recuperan los datos de TB si no son realmente necesarios. Además esta aproximación evita que si tenemos muchas tablas relacionadas, al hacer una consulta podríamos traernos a memoria prácticamente toda la base de datos.

Desde el punto de vista de la base de datos, esta aproximación es ineficiente, ya que se hacen demasiadas consultas para recuperar una información que podríamos obtener con una sola consulta. Mas adelante veremos que opciones tenemos para modificar este comportamiento.

4. Fetch & Lazy

Para entender mejor como se trae Hibernate la información de la base de datos, podemos hablar de dos conceptos: “cuando” se trae la información de la relación y “como” se la trae (es decir que SQL utiliza).

“Jugar” con estas dos ideas nos permite ajustar el rendimiento.

4.1. Lazy

Al especificar una relación en un mapeo de Hibernate usaremos el atributo “lazy” para definir el “cuando”.

Por defecto lazy es igual a true. Esto significa que la colección no se recupera de la base de datos hasta que se hace alguna operación sobre ella.

Si fijamos lazy a false, cuando se recupere la información de A también se traerá toda la información de los objetos relacionados de B (este era el comportamiento por defecto en la versión 2 de Hibernate).
<set name="bs" lazy="false">

4.2. Fetch

Por otro lado el atributo “fetch” define que SQLs se van a lanzar para recuperar la información.
Por defecto fetch es igual a select. Esto implica que para recuperar cada objeto de B se lanzará una nueva consulta a la base de datos.

Si fijamos el valor de fetch a join, en la misma consulta que se recupera la información de A también se recuperará la información de todos los objetos relacionados de B. Esto se consigue con un left outer join en la sentencia SQL.
<set name="bs" fetch="join">

4.3. Consideraciones sobre Join & Lazy

Nótese que los valores de lazy y fetch son independientes. Por ejemplo si fijamos lazy=”false” y dejamos fetch por defecto (fetch=”select”), implica que cuando se recuperen los datos de TA también se recuperan los de TB, pero se hará con 1+n consultas a la base de datos.

Por el contrario si fijamos el valor de fetch=”join” de forma implícita queda lazy=”false”. No tendría sentido de otra manera ya que al especificar fetch=”join” estamos recuperando toda la información en una sola consulta SQL.

También hay que tener en cuenta que, aunque fijemos el valor de fetch=”join”, sólo se hará una consulta SQL con left outer join si estamos en alguno de los siguientes casos (19.1.2. Tuning fetch strategies):
  • Estamos recuperando la información con get() o load()
  • Se hace una recuperación implícita por navegar por la asociación.
  • Consultas con Crtireia.
  • Consultas HQL si en la recuperación se usa subselect.
Es decir si hacemos una consulta HQL del estilo:
List ret = session.createQuery("from A").list();

no se va a utilizar un left outer join, sino que se hará una única consulta para recuperar los valores de TA, y más tarde, cuando se acceda a la colección se hará una nueva consulta para recuperar los valores de TB (estamos en la situación de 1+n consultas).

5. Como forzar un join en una consulta HQL

Independientemente de como hallamos definido el mapeo de la asociación entre A y B, podemos hacer un join explícitamente en una consulta HQL.

Basta con hacer:
List ret = session.createQuery("from A as a left join fetch a.bs").list();

Nótese la importancia de especificar la palabra reservada “fetch”. Con esto conseguimos que se nos devuelva una lista de objetos de tipo A donde su colección bs ya está cargada con los datos correspondientes de TB.

Si por el contrario no especificamos la palabra reservada “fetch”, se nos devolverá una lista donde cada elemento es a su vez otra lista de dos elementos, el primero (índice 0) contendrá el objeto de tipo A y el segundo (índice 1) contendrá el objeto de tipo B. Pero en el objeto de tipo A la colección bs estará a null. Es decir, estamos recuperando la información pero no se está creando la asociación entre los objetos de A y B.

6. Otras consideraciones sobre get() y load()

Teniendo en cuenta que en este tutorial hemos hablado de cuando se hacen las consultas a la base de datos, vamos a comentar la diferencia entre get() y load().

Cuando hacemos sesison.get() es porque no sabemos si el objeto que estamos solicitando existe o no en la base de datos, por lo tanto Hibernate hace la consulta a la base de datos de forma inmediata. Si no se encontrara el objeto en la base de datos, el método get() devuelve null.

Sin embargo al usar session.load() estamos dejando claro a Hibernate que sabemos con seguridad que el objeto que estamos solicitando sí que existe en la base de datos. En este caso Hibernate no hace la consulta de forma inmediata, sino que espera hasta que accedamos al objeto para hacer la consulta.

7. Conclusiones

Como podéis ver es fundamental conocer bien como funcionan las tecnologías que utilizamos. En el caso de Hibernate hemos visto que dependiendo de como hagamos las cosas puede afectar directamente al rendimiento de la aplicación.

Esto no quiere decir que no debamos usar Hibernate, al contrario, Hibernate nos proporciona grandes beneficios como es la independencia de la base de datos, bajo acoplamiento entre negocio y persistencia, y un desarrollo rápido, ya que con Hibernate podremos cubrir de manera sencilla y rápida el 80 - 90% de la persistencia de nuestra aplicación. Esto nos permite centrar nuestros esfuerzos en optimizar las consultas que realmente lo merecen (por supuesto siempre después de haber hecho un análisis y haber identificado cual es el cuello de botella).

Desde Autentia (http://www.autentia.com) os animamos a que utilicéis este tipo de tecnologías. Basta ya de reinventar la rueda en cada desarrollo. Debemos construir en función de patrones y estándares, y reutilizando al máximo. Esto nos permite centrarnos en la lógica de negocio, reducir los tiempos de desarrollo, y aumentar la calidad del resultado.

No hay comentarios.: