Curso Jakarta EE 9 (36). JPA con Hibernate (19): Criteria API (1). Consultas básicas.

logo Jakarta EE

Las consultas JPQL son simples cadenas de texto fáciles de escribir y comprender debido a sus similitudes con el lenguaje SQL. La alternativa dentro de Jakarta Persistence para explotar los datos con el modelo de entidades es la API Criteria. Permite construir consultas de forma programática, esto es, en código Java, y con un tipado absoluto tanto de los parámetros como de los resultados. Lo hace ofreciendo métodos que equivalen a las cláusulas, operadores y funciones de JPQL.

Daremos un paseo de dos capítulos por esta API tomando a JPQL como referencia. Puesto que el curso contiene tres capítulos introductorios a este lenguaje, no voy a repetirme y explicar de nuevo, por ejemplo, cómo funcionan los distintos tipos de reuniones de entidades. También aplicaremos los conocimientos de capítulos anteriores para ejecutar las consultas porque es algo que no cambia. Si el lector se siente perdido, le animo a revisar los últimos capítulos.

>>>> ÍNDICE <<<<

El primer ejemplo

Recordemos el modelo de entidades del proyecto jpa-query.

Lo mejor es comenzar con un ejemplo sencillo. Este método se encuentra en la clase ExpenseJpqlDAOImpl. Obtiene los gastos de forma paginada ordenados por fecha, nombre y cuantía.

    @Override
    public Page<Expense> findAll(int first, int max) {
        List<Expense> expenses = em.createQuery("SELECT e FROM Expense e " +
                "ORDER BY e.date DESC, e.concept, e.amount", Expense.class)
                .setFirstResult(first)
                .setMaxResults(max)
                .getResultList();
        return new Page<>(expenses, count(), first, max);
    }

El equivalente con Criteria API.

    @Override
    public Page<Expense> findAll(int first, int max) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Expense> cq = cb.createQuery(Expense.class);
        Root<Expense> expenseRoot = cq.from(Expense.class);

        cq.select(expenseRoot)
                .orderBy(
                        cb.desc(expenseRoot.get("date")),
                        cb.asc(expenseRoot.get("concept")));

        List<Expense> expenses = em.createQuery(cq).setFirstResult(first)
                .setMaxResults(max).getResultList();
        return new Page<>(expenses, count(), first, max);
    }

Empezamos con la creación de un objeto de la interfaz CriteriaQuery que usaremos como base para escribir la consulta. Si sabemos el tipo de retorno, lo creamos de forma tipada. Nuestro CriteriaQuery, y otros componentes que necesitaremos para construir la consulta, se obtienen mediante una instancia de CriteriaBuilder proporcionada por el gestor de entidades. El componente que siempre vamos a necesitar definir es el Root (raíz) de la consulta. Equivale a una entidad en el FROM, y lo usaremos como la variable de esa entidad para ir navegando por sus atributos. Será la base de nuestra consulta.

A continuación, he definido las cláusulas SELECT y ORDER BY de la consulta con métodos de CriteriaQuery. Dado que los métodos de esta clase devuelven la propia instancia, recomiendo ir encadenando las llamadas para hacer el código más simple. El primero de ellos no tiene misterio; proyectamos la entidad de la raíz. En cuanto a la ordenación, obtenemos con CriteriaBuilder un objeto de tipo Order para cada elemento ordenable con los métodos desc (ordenación descendente) y asc (ordenación ascendente). Como ya comenté, accedemos a los atributos de la variable que representa al gasto a través del objeto Root. Volveremos sobre este asunto más adelante.

Por último, ejecutamos la consulta del mismo modo que si se tratara de JPQL. Solo cambia el método del gestor de entidades que crea el objeto TypedQuery, (o Query si la consulta no es tipada).

Espero que con una imagen se aprecie mejor la equivalencia entre JPQL y Criteria API para el ejemplo. He separado las líneas de código para que quede más claro.

El resultado final, como es habitual en el curso, lo verificamos con una prueba. Aprovecho para recordar que, aunque no siempre aparezcan en el texto, la mayoría de métodos que se irán añadiendo a los DAO cuentan con tests.

class ExpenseCriteriaDAOTest extends BaseDaoTest {

    @Inject
    ExpenseCriteriaDao expenseCriteriaDao;

    @Test
    void testFindAll() {
       List<Expense> expenses = expenseCriteriaDAO.findAll(0, 6).getResults();

        assertThat(expenses)
                .extracting(Expense::getId)
                .containsExactly(Datasets.EXPENSE_ID_6, Datasets.EXPENSE_ID_5,
                        Datasets.EXPENSE_ID_3, Datasets.EXPENSE_ID_4,
                        Datasets.EXPENSE_ID_2, Datasets.EXPENSE_ID_1);
    }

Tipado con el metamodelo

En una cadena es posible escribir cualquier cosa y no sabremos si una consulta JPQL es sintácticamente correcta hasta que Hibernate la procese en tiempo de ejecución, o bien cuando se inicie la aplicación si hablamos de consultas nombradas. El plugin JPA Buddy de IntelliJ puede detectar las posibles incorrecciones, con algún que otro falso error, si está bien configurado.

Criteria API debería eliminar el problema anterior. No obstante, si nos fijamos en la cláusula ORDER BY siguen apareciendo cadenas en el código que pueden contener valores incorrectos. La realidad es que necesitamos escribir los nombres de los atributos cuando tengamos que hacer referencia a ellos.

La buena noticia es que podemos tener tipado «absoluto» de todos los elementos de la consulta si empleamos el llamado metamodelo de la unidad de persistencia. Se trata de una variada colección de datos que describe al modelo de entidades. Con él, no solo diremos adiós a las cadenas en nuestras consultas, sino que además evitaremos errores en tiempo de compilación cuando tengamos que trabajar con los atributos de las entidades.

Accedemos al metamodelo a través de un objeto de la interfaz Metamodel que obtendremos con el método EntityManager#getMetamodel. Sin embargo, para nuestro propósito es más práctico y cómodo generar el metamodelo estático, un conjunto de clases que representan a cada entidad y sus atributos. Hibernate lo pone fácil porque provee un plugin para Maven.

 <dependency>
     <groupId>org.hibernate</groupId>
     <artifactId>hibernate-jpamodelgen-jakarta</artifactId>
     <version>${hibernate.jakarta}</version>
     <scope>compile</scope>
</dependency>

Lo configuramos del siguiente modo para que la compilación ejecute la generación de código tanto de Lombok como del metamodelo en el orden adecuado.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven.compiler.version}</version>
    <configuration>
        <compilerArguments>
            <processor>lombok.launch.AnnotationProcessorHider$AnnotationProcessor, org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor
            </processor>
         </compilerArguments>
      </configuration>
</plugin>

El metamodelo también debe generarse cuando el IDE compile el proyecto. De no ser así, habrá que hacerlo manualmente con Maven. En IntelliJ, abriremos la configuración del proyecto en File->Settings para añadir en Annotations Processors el mismo que pusimos en el pom.

La configuración es algo más laboriosa en Eclipse. Abrimos las propiedades del proyecto y nos vamos a la pantalla correspondiente a Java Compiler->Annotation Processor. Marcamos todas las opciones y cambiamos la carpeta de generación de código automático a target/generated-sources/annotations/.

En Factory Path hay que agregar con el botón Add External Jar la librería que genera el metamodelo estático (hibernate-jpamodelgen). En la imagen, he seleccionado el .jar que tengo en mi repositorio local de Maven.

Encontramos las nuevas clases generadas por el plugin en la carpeta /target/generated/sources/annotations. Tendremos una por entidad, respetándose el paquete, y con el mismo nombre acompañado del sufijo «_»

package com.danielme.jakartaee.jpa.entities;

import jakarta.persistence.metamodel.SingularAttribute;
import jakarta.persistence.metamodel.StaticMetamodel;
import java.math.BigDecimal;
import java.time.LocalDate;
import javax.annotation.processing.Generated;

@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Expense.class)
public abstract class Expense_ {

	public static volatile SingularAttribute<Expense, LocalDate> date;
	public static volatile SingularAttribute<Expense, BigDecimal> amount;
	public static volatile SingularAttribute<Expense, String> comments;
	public static volatile SingularAttribute<Expense, String> concept;
	public static volatile SingularAttribute<Expense, Long> id;
	public static volatile SingularAttribute<Expense, Category> category;

	public static final String DATE = "date";
	public static final String AMOUNT = "amount";
	public static final String COMMENTS = "comments";
	public static final String CONCEPT = "concept";
	public static final String ID = "id";
	public static final String CATEGORY = "category";

}

Usemos las constantes de la clase anterior para reemplazar las cadenas.

 cq.orderBy(
            cb.desc(expenseRoot.get(Expense_.DATE)),
            cb.asc(expenseRoot.get(Expense_.CONCEPT)));

Es un avance, pero tampoco aporta demasiada seguridad porque podemos compilar lo siguiente (name no existe en Expense) y el error no será detectado hasta la ejecución de la consulta. A fin de cuentas, a cb.desc le seguimos proporcionando una cadena que puede contener cualquier cosa.

cq.orderBy(
            cb.desc(expenseRoot.get(Budget_.NAME)),
            cb.asc(expenseRoot.get(Expense_.CONCEPT)));

Hay una alternativa mejor. Los objetos SingularAttribute relacionan cada atributo con su tipo y entidad y pueden ser los argumentos del método Path#get. Ahora, si cambiamos la constante Budget_.NAME por Budget_.name la compilación falla. ¿El truco? El objeto expenseRoot se creó para la clase Expense, lo que implica que su get solo acepta los SingularAttribute para Expense.

Lo vemos más claro revisando la signatura de get, considerando que X es Expense. Recuerda que Root hereda de Path.

public interface Path<X> extends Expression<X> {
  <Y> Path<Y> get(SingularAttribute<? super X, Y> attribute);

SingularAttribute también nos dará seguridad cuando usemos un path en las evaluaciones que veremos en el próximo capítulo gracias al tipado del propio atributo. Me adelanto con en este ejemplo en el que evita que comparemos un String (el campo concept del gasto) con un BigDecimal.

public static volatile SingularAttribute<Expense, String> concept;

Con cadenas pasaríamos por alto el error porque esta línea es válida.

cb.lessThanOrEqualTo(expenseRoot.get(Expense_.CONCEPT), new BigDecimal("50.00"));

¡Seguridad al poder! En todas las consultas de tipo Criteria del curso recurriré al metamodelo.

Select

Las consultas que devuelven una entidad o un escalar no presentan muchas dificultades en lo que respecta a la definición de la cláusula SELECT y la recogida de los resultados. El método select recibe un único objeto de tipo Selection que modela la proyección. Según la siguiente jerarquía de interfaces, vemos que existen varios subtipos.

Uno de ellos es Root. Por ello, en el método findAll que mostré al principio del capítulo, para devolver la entidad raíz al completo se usó el objeto Root que la representa. Si necesitamos acceder a las propiedades de la entidad, y esto es válido en cualquier parte de la consulta, lo haremos con objetos de tipo Path. Es otro subtipo de Selection que construiremos con la ayuda del metamodelo estático y el método get del root.

    @Override
    public List<String> getConcepts() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<String> cq = cb.createQuery(String.class);
        Root<Expense> expenseRoot = cq.from(Expense.class);

        cq.select(expenseRoot.get(Expense_.concept));
        cq.orderBy(cb.asc(expenseRoot.get(Expense_.concept)));

         return em.createQuery(cq)
                .getResultList()
    }

El método getConcepts obtiene un listado con los conceptos de los gastos, así que creamos el objeto CriteriaQuery para String y construimos el Path correspondiente al atributo concept de Expense. Lo habitual es crear los objetos path en el lugar en el que se requieran, salvo que se reutilicen. Las líneas 7 y 8 de getConcepts pueden escribirse así.

Path<String> expConcept = expenseRoot.get(Expense_.concept);
cq.select(expConcept)
   .orderBy(cb.asc(expConcept));

Navegamos por las relaciones encadenando llamadas a distintos objetos Path, de igual forma que en JPQL usamos el operador «punto». Así accedemos al nombre de la categoría de un gasto.

Path<String> expCatName = expenseRoot
                .get(Expense_.category)
                .get(Category_.name);

Recordemos que lo anterior implica la reunión de Expense y Category. Si no la declaramos explícitamente, Hibernate lo hará por nosotros en la sentencia SQL generada.

Cuando necesitemos descartar duplicados, invocamos al método distinct.

cq.select(expenseRoot.get(Expense_.date)).distinct(true);

Las estructuras de tipo CASE…WHEN de JPQL son fáciles de trasladar a la API aplicando un estilo «fluido» de encadenados de llamadas que parten del método CriteriaBuilder#selectCase el cual construye un objeto Case que, por ser de tipo Expression, también es Selection. Recuperemos el ejemplo para JPQL que obtiene los datos de los gastos y una cadena que indica el tipo de gasto según su cantidad.

SELECT e.id, e.concept, e.category.name, e.amount, e.date, e.comments,
	CASE
		WHEN e.amount > 50 THEN 'EXPENSIVE'
        WHEN e.amount > 20 THEN 'STANDARD'
		ELSE 'SMALL'
	END
FROM	Expense e
ORDER BY e.date DESC, e.concept

Esta es la expresión. El resultado de la evaluación es una cadena que se corresponde con un valor del enumerado ExpenseDTO.ExpenseType.

Expression<String> selectCase = cb.selectCase()
                  .when(cb.ge(expenseRoot.get(Expense_.amount), 50),
                          ExpenseDTO.ExpenseType.EXPENSIVE.name())
                  .when(cb.ge(expenseRoot.get(Expense_.amount), 20),
                          ExpenseDTO.ExpenseType.STANDARD.name())
                  .otherwise(ExpenseDTO.ExpenseType.SMALL.name()));

El código al completo que recoge el resultado en la clase ExpenseDTO, siguiendo la técnica del constructor que veremos en la próxima sección, está en el método ExpenseCriteriaDAOImpl#findAllWithType del proyecto de ejemplo.

No pueden faltar las versiones «simplificadas» de CASE WHEN. COALESCE devuelve la primera expresión no nula.

SELECT COALESCE(e.comments, e.concept)
FROM Expense e
cq.select(
          cb.coalesce(
                  expenseRoot.get(Expense_.comments),
                  expenseRoot.get(Expense_.concept))
         );

Aunque el método CriteriaBuilder#coalesce solo admite dos argumentos, cabe la posibilidad de crear un objeto de la interfaz CriteriaBuilder.Coalesce, un subtipo de Expression, e ir concatenando en orden todas las expresiones a evaluar. El ejemplo anterior quedaría así (atención al tipado del objeto Coalesce).

cq.select(
         cb.<String>coalesce()
              .value(expenseRoot.get(Expense_.comments))
              .value(expenseRoot.get(Expense_.concept))
        );

Por su parte, NULLIF evalúa nulos de una forma peculiar: devuelve NULL si los dos valores escalares que recibe como argumento son iguales. En caso contrario, retorna el primero de ellos.

SELECT NULLIF(e.concept, e.comments) 
FROM Expense e
cq.select(
          cb.nullif(
                  expenseRoot.get(Expense_.comments),
                  expenseRoot.get(Expense_.concept))
         );

Proyección múltiple

Veamos qué hacer cuando la proyección cuente con más de un elemento como en este caso.

SELECT e.concept, e.amount, e.date 
FROM Expense e 
ORDER BY e.amount DESC, e.date DESC

La construcción de la cláusula SELECT no puede ser más sencilla. En lugar de select, se invoca a multiselect con todos los Selection que necesitemos.

cq.multiselect(
                expenseRoot.get(Expense_.concept),
                expenseRoot.get(Expense_.amount),
                expenseRoot.get(Expense_.date));

Lo que sí merece especial atención es la configuración y tratamiento del resultado. Tenemos disponibles las cuatro técnicas que vimos para JPQL: Object, Tuple, constructor y ResultTransformer. Por tanto, no voy a entrar en detalles y me limitaré a indicar cómo aplicarlas al ejemplo.

Object

Con este modo, cada resultado se encapsula en un array de Object el cual sigue el orden declarado en la proyección. Hay que asignar a CriteriaQuery el tipo Object[], si no se asume Object. Lo habitual será recoger los datos en clases de tipo DTO o records.

    @Override
    public List<ExpenseSummaryDTO> getSummary() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
        Root<Expense> expenseRoot = cq.from(Expense.class);
       
         cq.multiselect(
                expenseRoot.get(Expense_.concept),
                expenseRoot.get(Expense_.amount),
                expenseRoot.get(Expense_.date))
          .orderBy(
                  cb.desc(expenseRoot.get(Expense_.amount)),
                  cb.desc(expenseRoot.get(Expense_.date)));

        return em.createQuery(cq)
                 .getResultStream()
                .map(o -> new ExpenseSummaryDTO(
                        (String) o[0],
                        (BigDecimal) o[1],
                        (LocalDate) o[2]))
                .collect(Collectors.toList());
    }
Tuple

Con Tuple, el tratamiento del resultado resulta más amigable. Basta con cambiar el tipo de CriteriaBuilder.

    @Override
    public List<ExpenseSummaryDTO> getSummaryTuple() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Tuple> cq = cb.createQuery(Tuple.class);
        Root<Expense> expenseRoot = cq.from(Expense.class);

        cq.multiselect(
                expenseRoot.get(Expense_.concept).alias(Expense_.CONCEPT),
                expenseRoot.get(Expense_.amount).alias(Expense_.AMOUNT),
                expenseRoot.get(Expense_.date).alias(Expense_.DATE))
          .orderBy(
                  cb.desc(expenseRoot.get(Expense_.amount)),
                  cb.desc(expenseRoot.get(Expense_.date)));

        return em.createQuery(cq)
                .getResultStream()
                .map(t -> new ExpenseSummaryDTO(
                        t.get(0, String.class),
                        t.get(1, BigDecimal.class),
                        t.get(2, LocalDate.class)))
                .collect(Collectors.toList());
    }

Si lo prefieres, puedes usar CriteriaBuilder#createTupleQuery.

CriteriaQuery<Tuple> cq = cb.createTupleQuery();

Con Tuple se lee cada elemento de la proyección según su posición, empezando por cero. Mejor aún si se nombran con un alias. Podemos usar una cadena, pero para nuestro ejemplo nos viene perfecto el metamodelo. Lo aprovechamos también la hora de conocer la clase de cada propiedad.

cq.multiselect(expenseRoot.get(Expense_.concept).alias(Expense_.CONCEPT),
                expenseRoot.get(Expense_.amount).alias(Expense_.AMOUNT),
                expenseRoot.get(Expense_.date).alias(Expense_.DATE));

 return em.createQuery(cq).getResultList() 
                .getResultStream()
                .map(t -> new ExpenseSummaryDTO(
                        t.get(Expense_.CONCEPT, Expense_.concept.getJavaType()),
                        t.get(Expense_.AMOUNT, Expense_.amount.getJavaType()),
                        t.get(Expense_.DATE, Expense_.date.getJavaType())))
                .collect(Collectors.toList());
Constructor

La proyección directa en un constructor es la mejor solución si no hay que hacer un procesamiento de los valores devueltos. Nos aseguramos que exista uno para la clase con la que hemos tipado CriteriaQuery que se corresponda con los elementos de multiselect.

    @Override
    public List<ExpenseSummaryDTO> getSummaryConstructor() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<ExpenseSummaryDTO> cq = cb.createQuery(ExpenseSummaryDTO.class);
        Root<Expense> expenseRoot = cq.from(Expense.class);

         cq.multiselect(
                expenseRoot.get(Expense_.concept),
                expenseRoot.get(Expense_.amount),
                expenseRoot.get(Expense_.date))
          .orderBy(
                  cb.desc(expenseRoot.get(Expense_.amount)),
                  cb.desc(expenseRoot.get(Expense_.date)));

        return em.createQuery(cq).getResultList();
    }
public ExpenseSummaryDTO(String concept, BigDecimal amount, java.sql.Date date) {
    this(concept, amount, date.toLocalDate());
}

Es incluso más sencillo que hacerlo con JPQL porque no es necesario utilizar el operador NEW (ni siquiera existe en la API).

SELECT NEW com.danielme.jakartaee.jpa.dto.ExpenseSummaryDTO(e.concept, e.amount, e.date)
FROM Expense e 
ORDER BY e.amount DESC, e.date DESC

ResultTransformer

Fuera de la especificación, vimos en el capítulo 33 que Hibernate cuenta con los transformadores de resultados. Al igual que sucede con JPQL, no aportan gran cosa, más allá de la estandarización del tratamiento en bruto cuando este sea necesario mediante la implementación de un ResultTransformer personalizado. La disponibilidad de la proyección en constructor hace innecesario el empleo de AliasToBeanResultTransformer y AliasToBeanConstructorResultTransformer.

Con respecto a su uso en Criteria API, simplemente indicar que la consulta debe tiparse con Object[]. A partir de ahí, procedemos como ya sabemos. Convertiremos el objeto de la interfaz Query en la implementación de Hibernate para invocar a setResultTransformer.

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Expense> expenseRoot = cq.from(Expense.class);

cq.multiselect(
         expenseRoot.get(Expense_.concept),
         expenseRoot.get(Expense_.amount),
         expenseRoot.get(Expense_.date))
   .orderBy(
         cb.desc(expenseRoot.get(Expense_.amount)),
         cb.desc(expenseRoot.get(Expense_.date)));

return em.createQuery(cq)
           .unwrap(org.hibernate.query.Query.class)
           .setResultTransformer(new ExpenseSummaryTransformer())
          .getResultList();

Reuniones

Las reuniones de entidades, basadas en las relaciones, se crean con el método join de la interfaz From. La hemos usado indirectamente, pues Root hereda de ella. Asimismo, join devuelve objetos de la interfaz Join, otra especialización de From, a la que iremos enlazando otras reuniones cuando sea necesario. También podemos emplear ese objeto retornado por join para hacer referencia a la entidad relacionada y sus atributos en cualquier expresión.

La explicación anterior dice muy poco; veamos algunos ejemplos. Esta es la consulta JPQL con la que ilustré la reunión interna o INNER JOIN de dos entidades, la más común. Recupera todas las categorías con un gasto de al menos «minAmount» unidades monetarias.

SELECT DISTINCT c
FROM Category c INNER JOIN c.expenses e 
WHERE e.amount >= :minAmount
ORDER BY c.name

Lo que haremos es invocar al método join del Root de Expense indicando el punto de reunión de las entidades Category y Expense y, opcionalmente, el tipo de reunión con un valor del enumerado JoinType. De este modo creamos un objeto que representa la reunión y que se usará para la selección que aparece en el WHERE.

    @Override
    public List<Category> findCategoriesByMinExpenseAmount(BigDecimal minAmount) {
       CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Category> cq = cb.createQuery(Category.class);
        Root<Category> categoryRoot = cq.from(Category.class);

        ListJoin<Category, Expense> expenses = categoryRoot.join(Category_.expenses);

        cq.select(categoryRoot)
          .where(cb.ge(expenses.get(Expense_.amount), minAmount)))
          .orderBy(cb.asc(categoryRoot.get(Category_.name)));

        return em.createQuery(cq)
                 .getResultList();
    }

No indiqué el tipo de reunión porque INNER JOIN es el predeterminado. Tendremos que hacerlo si queremos realizar una reunión externa como la que sigue.

SELECT NEW com.danielme.jakartaee.jpa.dto.CouponSummaryDTO(c.id, c.name, e.concept, e.date)
FROM Coupon c LEFT JOIN c.expense e 
ORDER BY c.name

Obtenemos en un DTO todos los cupones (izquierda del JOIN) y el gasto de cada uno cuando exista.

    @Override
    public List<CouponSummaryDTO> getSummary() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<CouponSummaryDTO> cq = cb.createQuery(CouponSummaryDTO.class);
        Root<Coupon> couponRoot = cq.from(Coupon.class);

        Join<Coupon, Expense> expense = couponRoot.join(Coupon_.expense, JoinType.LEFT);

        cq.multiselect(
                couponRoot.get(Coupon_.id),
                couponRoot.get(Coupon_.name),
                expense.get(Expense_.concept),
                expense.get(Expense_.date))
          .orderBy(cb.asc(couponRoot.get(Coupon_.name)));

        return em.createQuery(cq).getResultList();
    }

Un detalle interesante del método anterior: en el multiselect obtenemos el Path de los atributos que no están situados en la entidad raíz mediante el objeto Join (líneas 11 y 12).

Si bien el JoinType cuenta con la opción RIGHT OUTER JOIN, los proveedores de JPA no están obligados a implementarla. Hibernate no lo hace, lo cual es un poco extraño porque HQL sí que admite este tipo de reunión.

java.lang.UnsupportedOperationException: RIGHT JOIN not supported

La recuperación de las relaciones perezosas es algo distinta. Lo que hacemos en JPQL es declarar una reunión, ya sea interna o externa, y añadir el operador FETCH.

SELECT c 
FROM Coupon c LEFT JOIN FETCH c.expense e
ORDER BY c.name

En esta ocasión, en vez de invocar al método join del Root, llamaremos a fetch para declarar la reunión. Retorna un objeto Fetch al que no podemos concatenar otras reuniones.

Root<Coupon> couponRoot = cq.from(Coupon.class);
couponRoot.fetch(Coupon_.expense, JoinType.LEFT);

Veamos ahora el producto cartesiano o CROSS JOIN de dos entidades que realiza todas las combinaciones posibles entre los registros de las tablas. No lo encontraremos en JoinType; tendremos que crear un Root para cada entidad y aplicar en el WHERE la selección que se desee. Simulemos una reunión interna entre gasto y categoría seleccionando los resultados en los que ambas entidades quedan emparejadas por la relación que tienen.

SELECT e 
FROM Expense  e, Category c 
WHERE e.category.id = c.id AND c.id=:id 
ORDER BY e.date DESc, e.concept
    @Override
    public List<Expense> findByCategory(Long id) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Expense> cq = cb.createQuery(Expense.class);
        Root<Expense> expenseRoot = cq.from(Expense.class);

        Root<Category> categoryRoot = cq.from(Category.class);

        cq.select(expenseRoot)
          .where(
                  cb.equal(categoryRoot.get(Category_.id), expenseRoot.get(Expense_.category).get(Category_.id)),
                  cb.equal(categoryRoot.get(Category_.id), id)
          )
          .orderBy(
                  cb.desc(expenseRoot.get(Expense_.date)),
                  cb.asc(expenseRoot.get(Expense_.concept)));

        return em.createQuery(cq).getResultList();
    }

Código de ejemplo

El código de ejemplo se encuentra en GitHub (todos los proyectos son independientes pero están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.