Hibernate y Joins con la clase Criteria

3
94149

Creación: 20-06-2007

Índice de contenidos

1. Introducción

Dentro de Hibernate (http://www.hibernate.org/) podemos encontrar la interfaz Criteria
(org.hibernate.Criteria). Esta interfaz nos permite especificar consultas programáticamente (en base a clases y métodos de estas clases) sobre nuestras entidades definiendo un conjunto de restricciones.

El uso de la clase Criteria puede ser muy conveniente cuando tenemos que componer consultas de forma dinámica, por ejemplo con las típicas pantallas de búsqueda, donde el usuario introduce una serie de criterios. En estos casos en vez de ir componiendo un String con el HQL en función de los datos introducidos por el usuario, puede resultar más cómodo usar la clase Criteria.

En este tutorial vamos a ver como hacer «joins» entre entidades relacionadas, y las implicaciones que esto puede tener.

Este tutorial se basa en el tutorial
http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=hibernateAnnotations.
El tutorial esta desarrollado con JUnit 4 (http://www.junit.org/), la sesión de Hibernate se abre y se cierra por cada método de test, y todas las operaciones se hacen dentro de una transacción.

Aquí podéis encontrar un archivo comprimido con todo el código. Es un proyecto de Maven (http://maven.apache.org/), así que las dependencias necesarias para compilar y ejecutar se os bajarán de Internet.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Asus G1 (Core 2 Duo a 2.1 GHz, 2048 MB RAM, 120 GB HD).
  • Sistema Operativo: GNU / Linux, Debian (unstable), Kernel 2.6.21, KDE 3.5
  • Máquina Virtual Java: JDK 1.6.0-b105 de Sun Microsystems
  • Eclipse 3.2.2
  • Hibernate 3.2.2.ga
  • Hibernate Tools 3.2.0 Beta9
  • MySql 5.0.41-2
  • JUnit 4.3.1
  • Maven 2.0.7

3. Usando Criteria.createCriteria()

En la primera aproximación vamos a usar el siguiente código.

@Test
public void criteriaTripleJoin() {
    log.info("nn*** criteriaTripleJoin ***n");

    Criteria criteria = session.createCriteria(Campaign.class)
        .createCriteria("subcampaigns")
        .createCriteria("budgets")
        .setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY) 
        ;

    List campaigns = criteria.list();
    prettyPrinter(campaigns); 
}

Si observamos el resultado volcado por prettyPrinter (también se están volcando las consultas que hace Hibernate, ya que en el fichero de configuración hibernate.cfg.xml
tenemos activa la opción <property name=«show_sql»>true</property>):

*** criteriaTripleJoin ***

Hibernate: select this_.id as id6_2_, this_.name as name6_2_, subcampaig1_.id as id7_0_, 
subcampaig1_.campaignId as campaignId7_0_, subcampaig1_.name as name7_0_,budget2_.id as id8_1_, 
budget2_.name as name8_1_, budget2_.subcampaignId as subcampa3_8_1_ from curso.Campaign this_ 
inner join curso.Subcampaign subcampaig1_ on this_.id=subcampaig1_.campaignId inner join 
curso.Budget budget2_ on subcampaig1_.id=budget2_.subcampaignId

- Campaign { id=1, name=Campaña 1 }

Hibernate: select subcampaig0_.campaignId as campaignId1_, subcampaig0_.id as id1_, 
subcampaig0_.id as id7_0_, subcampaig0_.campaignId as campaignId7_0_, 
subcampaig0_.name as name7_0_ from curso.Subcampaign subcampaig0_ 
where subcampaig0_.campaignId=?

-     Subcampaign { id=2, name=Campaña 1 Subcampaña 2 }

Hibernate: select budgets0_.subcampaignId as subcampa3_1_, budgets0_.id as id1_, 
budgets0_.id as id8_0_, budgets0_.name as name8_0_, budgets0_.subcampaignId as subcampa3_8_0_ 
from curso.Budget budgets0_ where budgets0_.subcampaignId=?

-     Subcampaign { id=3, name=Campaña 1 Subcampaña 3 }

Hibernate: select budgets0_.subcampaignId as subcampa3_1_, budgets0_.id as id1_, 
budgets0_.id as id8_0_, budgets0_.name as name8_0_, budgets0_.subcampaignId as subcampa3_8_0_ 
from curso.Budget budgets0_ where budgets0_.subcampaignId=?

-         Budget { id=6, name=Campaña 1 Subcampaña 3 Presupuesto 3 }
-         Budget { id=5, name=Campaña 1 Subcampaña 3 Presupuesto 2 }
-         Budget { id=4, name=Campaña 1 Subcampaña 3 Presupuesto 1 }
-     Subcampaign { id=1, name=Campaña 1 Subcampaña 1 }

Hibernate: select budgets0_.subcampaignId as subcampa3_1_, budgets0_.id as id1_, 
budgets0_.id as id8_0_, budgets0_.name as name8_0_, budgets0_.subcampaignId as subcampa3_8_0_ 
from curso.Budget budgets0_ where budgets0_.subcampaignId=?

-         Budget { id=1, name=Campaña 1 Subcampaña 1 Presupuesto 1 }
-         Budget { id=3, name=Campaña 1 Subcampaña 1 Presupuesto 3 }
-         Budget { id=2, name=Campaña 1 Subcampaña 1 Presupuesto 2 }
- Campaign { id=2, name=Campaña 2 }

Hibernate: select subcampaig0_.campaignId as campaignId1_, subcampaig0_.id as id1_, 
subcampaig0_.id as id7_0_, subcampaig0_.campaignId as campaignId7_0_, 
subcampaig0_.name as name7_0_ from curso.Subcampaign subcampaig0_ 
where subcampaig0_.campaignId=?

-     Subcampaign { id=4, name=Campaña 2 Subcampaña 1 }

Hibernate: select budgets0_.subcampaignId as subcampa3_1_, budgets0_.id as id1_, 
budgets0_.id as id8_0_, budgets0_.name as name8_0_, budgets0_.subcampaignId as subcampa3_8_0_ 
from curso.Budget budgets0_ where budgets0_.subcampaignId=?

-         Budget { id=7, name=Campaña 2 Subcampaña 1 Presupuesto 1 }
-     Subcampaign { id=5, name=Campaña 2 Subcampaña 2 }

Hibernate: select budgets0_.subcampaignId as subcampa3_1_, budgets0_.id as id1_,
 budgets0_.id as id8_0_, budgets0_.name as name8_0_, budgets0_.subcampaignId as subcampa3_8_0_ 
 from curso.Budget budgets0_ where budgets0_.subcampaignId=?

Podemos observar como, cada vez que se entra en una relación se vuelve a realizar una consulta contra la base de datos para recuperar la información. Esto se debe a que las relaciones están configuradas con lazy=true (ver tutorial
http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=hibernateJoin),
y cada vez que se navega por la relación (getSubcampaigns() o getBudgets()) se vuelve a hacer una consulta para recuperar la información.

Pero entonces ¿para que sirve el método createCriteria()? Bien, el método
createCriteria() sirve para definir nuevas restricciones sobre las clases de la relación, pero el uso de createCriteria() no quiere decir que se recuperen todos los datos de la relación, de hecho no se recuperará ninguno.

De hecho, en nuestro ejemplo, como no especificamos ninguna restricción sobre ninguna de las clases relacionadas, podríamos haber quitado el createCriteria("subcampaigns")
y el createCriteria("budgets").

4. Usando Criteria.setFetchMode

Este segundo ejemplo es muy similar al anterior, salvo que hemos cambiado los createCriteria() por setFetchMode(). Cabe destacar en este caso que para hacer referencia los presupuestos lo hacemos a través de «subcampaigns.budgets«, es decir, los setFetchMode() siempre se refieren a la entidad definida en el último createCriteria (en nuestro ejemplo session.createCriteria(Campaign.class)):

@Test
public void criteriaTripleJoinWithFetch() {
    log.info("nn*** criteriaTripleJoinWithFetch ***n");

    Criteria criteria = session.createCriteria(Campaign.class)
        .setFetchMode("subcampaigns", FetchMode.JOIN)
        .setFetchMode("subcampaigns.budgets", FetchMode.JOIN)
        .setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY)
        ;

    List campaigns = criteria.list();
    prettyPrinter(campaigns);
}

Si ahora observamos la salida, veremos:

*** criteriaTripleJoinWithFetch ***

Hibernate: select this_.id as id6_2_, this_.name as name6_2_, 
subcampaig2_.campaignId as campaignId4_, subcampaig2_.id as id4_, 
subcampaig2_.id as id7_0_, subcampaig2_.campaignId as campaignId7_0_, 
subcampaig2_.name as name7_0_, budgets3_.subcampaignId as subcampa3_5_,
 budgets3_.id as id5_, budgets3_.id as id8_1_, budgets3_.name as name8_1_, 
  budgets3_.subcampaignId as subcampa3_8_1_ from curso.Campaign this_ 
  left outer join curso.Subcampaign subcampaig2_ on this_.id=subcampaig2_.campaignId 
left outer join curso.Budget budgets3_ on subcampaig2_.id=budgets3_.subcampaignId

- Campaign { id=1, name=Campaña 1 }
-     Subcampaign { id=2, name=Campaña 1 Subcampaña 2 }
-     Subcampaign { id=1, name=Campaña 1 Subcampaña 1 }
-         Budget { id=3, name=Campaña 1 Subcampaña 1 Presupuesto 3 }
-         Budget { id=2, name=Campaña 1 Subcampaña 1 Presupuesto 2 }
-         Budget { id=1, name=Campaña 1 Subcampaña 1 Presupuesto 1 }
-     Subcampaign { id=3, name=Campaña 1 Subcampaña 3 }
-         Budget { id=4, name=Campaña 1 Subcampaña 3 Presupuesto 1 }
-         Budget { id=5, name=Campaña 1 Subcampaña 3 Presupuesto 2 }
-         Budget { id=6, name=Campaña 1 Subcampaña 3 Presupuesto 3 }
- Campaign { id=2, name=Campaña 2 }
-     Subcampaign { id=5, name=Campaña 2 Subcampaña 2 }
-     Subcampaign { id=4, name=Campaña 2 Subcampaña 1 }
-     Budget { id=7, name=Campaña 2 Subcampaña 1 Presupuesto 1 }

Esta vez parece que si está funcionando correctamente, se está haciendo una única consulta al principio, y luego se navega por todas las relaciones, pero esta vez ya no hace nuevas consultar a la base de datos. Con setFetchMode() estamos definiendo como se quieren recuperar los objetos de las relaciones, ignorando la configuración que tengamos en los atributos lazy o fetch.

Sin embargo no podemos cantar victoria tan rápido. Si os habéis fijado en ambos ejemplos usamos
setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY),
con esto estamos indicando a Hibernate que queremos que nos devuelva sólo las entidades diferentes de la entidad raíz (Campaign.class). El problema que tiene esto es que la búsqueda de entidades diferentes se hace en memoria, es decir, Hibernate en vez de usar un «distinct» en la sentencia SQL, se trae todos los resultado para luego hacer la búsqueda en memoria. Esto puede ser muy desaconsejable, ya que se podría dar el caso de traernos multitud de resultados cuando sólo un pequeño porcentaje son realmente
diferentes.

Solucionar esto con la clase Criteria no es tarea fácil. En el próximo ejemplo se ve una aproximación.

5. Usando proyecciones

Con la clase Projection, podemos definir consultar escalares, es decir, consultas donde no nos traemos entidades enteras, sino que definimos uno a uno los distintos campos que queremos recuperar de la base de datos

@Test
public void projection() {
    log.info("nn*** projection ***n");

    Criteria criteria = session.createCriteria(Campaign.class)
        .setProjection(Projections.distinct(Projections.projectionList()
            .add(Projections.property("id"), "id")
            .add(Projections.property("name"), "name")
        )).setResultTransformer(Transformers.aliasToBean(Campaign.class))
        ;

    List campaigns = criteria.list();
    prettyPrinter(campaigns);
}

Si vemos la salida:

*** projection ***

*** projection ***
Hibernate: select distinct this_.id as y0_, this_.name as y1_ from curso.Campaign this_

- Campaign { id=1, name=Campaña 1 }
- Campaign { id=2, name=Campaña 2 }

Podemos observar como sólo se ha traído la entidad raíz (Campaign.class), y como además las relaciones no son navegables!!! Es decir, aunque hemos usado un Transformer para convertir las columnas sueltas en una entidad correspondiente, estos objetos no se ven como objetos de Hibernate, por lo que las operaciones que hacemos sobre ellos son ignoradas por Hibernate.

En el foro http://forum.hibernate.org/viewtopic.php?t=941669, podemos encontrar más información sobre el uso de Projection, pero veremos que su uso es complicado, y rara vez conseguiremos obtener lo que realmente buscamos.

6. Usando HQL

Parece que en esta ocasión, cuando queremos hacer joins y navegar por las relaciones sin que Hibernate vuelva a lanzar consultas contra la base de datos, la clase Criteria se nos queda un poco corta. En este último ejemplo mostramos como hacerlo con HQL:

@Test
public void hqlJoin() {
    log.info("nn*** hqlJoin ***n");

    String hql = "select distinct c from Campaign c join fetch c.subcampaigns s join fetch s.budgets";

    Query query = session.createQuery(hql);

    List campaigns = query.list();
    prettyPrinter(campaigns);
}

Si inspeccionamos la salida nos encontraremos con:

*** hqlJoin ***

Hibernate: select distinct campaign0_.id as id6_0_, subcampaig1_.id as id7_1_,
 budgets2_.id as id8_2_, campaign0_.name as name6_0_, 
 subcampaig1_.campaignId as campaignId7_1_, subcampaig1_.name as name7_1_,
 subcampaig1_.campaignId as campaignId0__, subcampaig1_.id as id0__, 
 budgets2_.name as name8_2_, budgets2_.subcampaignId as subcampa3_8_2_, 
 budgets2_.subcampaignId as subcampa3_1__, budgets2_.id as id1__ from 
 curso.Campaign campaign0_ inner join curso.Subcampaign subcampaig1_ on 
 campaign0_.id=subcampaig1_.campaignId inner join curso.Budget budgets2_ 
 on subcampaig1_.id=budgets2_.subcampaignId

- Campaign { id=1, name=Campaña 1 }
-     Subcampaign { id=3, name=Campaña 1 Subcampaña 3 }
-         Budget { id=6, name=Campaña 1 Subcampaña 3 Presupuesto 3 }
-         Budget { id=5, name=Campaña 1 Subcampaña 3 Presupuesto 2 }
-         Budget { id=4, name=Campaña 1 Subcampaña 3 Presupuesto 1 }
-     Subcampaign { id=1, name=Campaña 1 Subcampaña 1 }
-         Budget { id=3, name=Campaña 1 Subcampaña 1 Presupuesto 3 }
-         Budget { id=2, name=Campaña 1 Subcampaña 1 Presupuesto 2 }
-         Budget { id=1, name=Campaña 1 Subcampaña 1 Presupuesto 1 }
- Campaign { id=2, name=Campaña 2 }
-     Subcampaign { id=4, name=Campaña 2 Subcampaña 1 }
-         Budget { id=7, name=Campaña 2 Subcampaña 1 Presupuesto 1 }

Como podemos ver es justo lo que necesitábamos. Con una sola consulta a la base de datos, obtenemos las entidades con las relaciones debidamente rellenas, y el distinct se está haciendo en la base de datos, y no en memoria.

7. Conclusiones

Ya hemos visto varias capacidades de Hibernate, y la clase Criteria es otra de ellas. También hemos comentado las ventajas de la clase Criteria y en que ocasiones puede ser conveniente su uso. Pero siempre tenemos que tener presente el antipatrón del Martillo de Oro (Golde Hammer): Cuando tu única herramienta es un martillo todo parecen clavos.

Es decir, no debemos querer solucionar todos los problemas de la misma manera. Hay que intentar elegir la solución más adecuada para cada ocasión, y en esta ocasión se ha
demostrado que la clase Criteria no es la solución más adecuada, sino que es mucho más sencillo y efectivo usar HQL.

8. Sobre el autor

Alejandro Pérez García, Ingeniero en Informática (especialidad de Ingeniería del Software)

Socio fundador de Autentia (Formación, Consultoría, Desarrollo de sistemas transaccionales)

mailto:alejandropg@autentia.com

Autentia Real Business Solutions S.L. – «Soporte a Desarrollo»

http://www.autentia.com

 

Alejandro es socio fundador de Autentia y nuestro experto en Java EE, Linux y optimización de aplicaciones empresariales. Ingeniero en Informática y Certified ScrumMaster. Seguir @alejandropgarci Si te gusta lo que ves, puedes contratarle para darte ayuda con soporte experto, impartir cursos presenciales en tu empresa o para que realicemos tus proyectos como factoría (Madrid). Puedes encontrarme en Autentia: Ofrecemos servicios de soporte a desarrollo, factoría y formación.

3 COMENTARIOS

  1. Complejillo de leer, requiere alto nivel de procesamiento matemático.
    Por otra parte muy agradecido Alejando y encantado de seguir viéndote haciendo movidas.

  2. Excelente explicación, tenemos ese problema que toooodo lo queremos resolver con criteria y a veces nos complicamos innecesariamente, cuando hay caminos mucho más faciles. Tenemos multiplies resultados por el mal uso de projections y criteria en aplicaciones viejitas, asi que será momento de actualizarlas, Gracias por tus aportes.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad