Creación: 20-06-2007
Índice de contenidos
- 1.Introducción
- 2.Entorno
- 3.Usando Criteria.createCriteria()
- 4.Usando Criteria.setFetchMode
- 5.Usando proyecciones
- 6.Usando HQL
- 7.Conclusiones
- 8. Sobre el autor
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
https://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
https://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»
Complejillo de leer, requiere alto nivel de procesamiento matemático.
Por otra parte muy agradecido Alejando y encantado de seguir viéndote haciendo movidas.
Muchas gracias, me ha sacado usted de un buen lío.
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.