Como ejecutar una query que devuelve un objeto custom en Spring Data JPA


Muchos nos encontramos que con Spring Data JPA, muchas de las queries que queremos lanzar son semi-automáticas y todo se hace simplemente definiendo nombres de métodos. Pero…​ ¿qué pasa cuando queremos hacer queries más complejas y devolver objetos que no son directamente el propio objeto del Repository? Queries con GROUP BY, con HAVING, con múltiples JOINs o funciones de agregación…​

Disponemos de varias opciones para devolver objetos custom en cualquiera de nuestros repositorios.

Crear la clase del objeto que queremos devolver

En primer lugar deberemos crear el objeto custom que queremos devolver en la query:

package com.devutil.examples.spring.jpa.repository.customobjects;

import lombok.Data;

@Data
public class EntidadNombre {

    private final String nombre;

    private final Long contador;

}

Para simplificar el ejemplo anterior, hemos usado lombok para la generación automática de getters, setters y constructor.

Definir la query en nuestro Repository

Lo siguiente que tenemos que hacer es definir un método en nuestra interfaz que implementa CrudRepository (org.springframework.data.repository.CrudRepository) y mediante la anotación @Query introducir nuestra SQL indicando en el SELECT el new de nuestra clase custom con las distintas variables y proyecciones de la consulta:

public interface EntidadRepository extends CrudRepository<Entidad, Integer> {

  @Query("SELECT new com.devutil.examples.spring.jpa.repository.customobjects.EntidadNombre(e.nombre, COUNT(*)) "
       + "FROM Entidad e "
       + "GROUP BY e.nombre "
       + "HAVING COUNT(*) > 1 "
       + "ORDER BY 2 DESC, 1 ASC")
  List<EntidadNombre> calcularGroupByEntidadNombre();

Al hacer el new de la entidad en la propia @Query, Spring Data nos devolverá automáticamente la lista de nuestros objetos custom.

Pero, existen más posibilidades de devolver estos datos sin tener que crear dicho objeto.

Devolver List<Object[]> en nuestro Repository

Si preferimos no crear ningún objeto ni interfaz, podemos hacer que nuestro Repository devuelva directamente una lista de Object[]:

public interface EntidadRepository extends CrudRepository<Entidad, Integer> {

  @Query("SELECT e.nombre, COUNT(*) "
       + "FROM Entidad e "
       + "GROUP BY e.nombre "
       + "HAVING COUNT(*) > 1 "
       + "ORDER BY 2 DESC, 1 ASC")
  List<Object[]> calcularGroupByNombre();

El mayor problema de este ejemplo es que no disponemos del tipo de cada elemento del array y tendremos que hacer múltiples casts e if’s en nuestro código.

Sin embargo, disponemos aún de otra opción que se queda a medio camino.

Crear interfaz para devolver como resultado

Spring Data nos permite definir una interfaz con los métodos necesarios para resolver los tipos de nuestras columnas de manera automática.

public interface IEntidadNombre {

    String getNombre();

    Integer getContador();

}

Únicamente tendremos que definir los métodos que queremos tener disponibles tras ejecutar la query fijando el tipo que deseamos. A continuación definimos nuestro método en el Repository del siguiente modo:

public interface EntidadRepository extends CrudRepository<Entidad, Integer> {

  @Query("SELECT e.nombre AS nombre, COUNT(*) AS contador "
       + "FROM Entidad e "
       + "GROUP BY e.nombre "
       + "HAVING COUNT(*) > 1 "
       + "ORDER BY 2 DESC, 1 ASC")
  List<IEntidadNombre> calcularGroupByIEntidadNombre();

Fijándonos bien en que el nombre que asignamos en el SELECT se corresponde con un getter de nuestra interfaz.

Aquí cabe destacar que las interfaces mejoran un aspecto que mediante la creación del objeto custom no se podría. Resulta que el COUNT(*) devuelve siempre Long, por lo que si nuestro objeto EntidadNombre hubiera tenido el field contador como Integer, hubiera fallado en runtime la ejecución de la query con la siguiente exception:

org.hibernate.hql.internal.ast.DetailedSemanticException:
    Unable to locate appropriate constructor on class
    [com.devutil.examples.spring.jpa.repository.customobjects.EntidadNombre].
    Expected arguments are: java.lang.String, long

Como has podido ver, Spring Data JPA nos proporciona varias opciones de cara a devolver resultados custom para queries y consultas con funciones de agregación de manera fácil y rápida.

Puedes ver un ejemplo completo en Github.

Icon