Cómo devolver un objeto personalizado de una consulta de Spring Data JPA GROUP BY

115

Estoy desarrollando una aplicación Spring Boot con Spring Data JPA. Estoy usando una consulta JPQL personalizada para agrupar por algún campo y obtener el recuento. A continuación se muestra mi método de repositorio.

@Query(value = "select count(v) as cnt, v.answer from Survey v group by v.answer")
public List<?> findSurveyCount();

Está funcionando y el resultado se obtiene de la siguiente manera:

[
  [1, "a1"],
  [2, "a2"]
]

Me gustaría obtener algo como esto:

[
  { "cnt":1, "answer":"a1" },
  { "cnt":2, "answer":"a2" }
]

¿Cómo puedo conseguir esto?

Pranav C Balan
fuente

Respuestas:

249

Solución para consultas JPQL

Esto es compatible con consultas JPQL dentro de la especificación JPA .

Paso 1 : declarar una clase de frijol simple

package com.path.to;

public class SurveyAnswerStatistics {
  private String answer;
  private Long   cnt;

  public SurveyAnswerStatistics(String answer, Long cnt) {
    this.answer = answer;
    this.count  = cnt;
  }
}

Paso 2 : devolver instancias de bean desde el método de repositorio

public interface SurveyRepository extends CrudRepository<Survey, Long> {
    @Query("SELECT " +
           "    new com.path.to.SurveyAnswerStatistics(v.answer, COUNT(v)) " +
           "FROM " +
           "    Survey v " +
           "GROUP BY " +
           "    v.answer")
    List<SurveyAnswerStatistics> findSurveyCount();
}

Notas importantes

  1. Asegúrese de proporcionar la ruta completa a la clase de bean, incluido el nombre del paquete. Por ejemplo, si se llama a la clase de bean MyBeany está en el paquete com.path.to, la ruta completa al bean será com.path.to.MyBean. Simplemente proporcionar MyBeanno funcionará (a menos que la clase de bean esté en el paquete predeterminado).
  2. Asegúrese de llamar al constructor de la clase bean usando la newpalabra clave. SELECT new com.path.to.MyBean(...)funcionará, mientras SELECT com.path.to.MyBean(...)que no.
  3. Asegúrese de pasar los atributos exactamente en el mismo orden que el esperado en el constructor del bean. Si intenta pasar atributos en un orden diferente, se producirá una excepción.
  4. Asegúrese de que la consulta sea una consulta JPA válida, es decir, que no sea una consulta nativa. @Query("SELECT ...")O @Query(value = "SELECT ..."), o @Query(value = "SELECT ...", nativeQuery = false)va a trabajar, mientras que @Query(value = "SELECT ...", nativeQuery = true)no va a funcionar. Esto se debe a que las consultas nativas se pasan sin modificaciones al proveedor JPA y se ejecutan contra el RDBMS subyacente como tal. Dado que newy com.path.to.MyBeanno son palabras clave SQL válidas, RDBMS lanza una excepción.

Solución para consultas nativas

Como se señaló anteriormente, la new ...sintaxis es un mecanismo compatible con JPA y funciona con todos los proveedores de JPA. Sin embargo, si la consulta en sí no es una consulta JPA, es decir, es una consulta nativa, la new ...sintaxis no funcionará ya que la consulta se pasa directamente al RDBMS subyacente, que no comprende la newpalabra clave ya que no es parte de el estándar SQL.

En situaciones como estas, las clases de bean deben reemplazarse con interfaces Spring Data Projection .

Paso 1 : declare una interfaz de proyección

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Paso 2 : devolver las propiedades proyectadas de la consulta

public interface SurveyRepository extends CrudRepository<Survey, Long> {
    @Query(nativeQuery = true, value =
           "SELECT " +
           "    v.answer AS answer, COUNT(v) AS cnt " +
           "FROM " +
           "    Survey v " +
           "GROUP BY " +
           "    v.answer")
    List<SurveyAnswerStatistics> findSurveyCount();
}

Utilice la ASpalabra clave SQL para asignar campos de resultados a propiedades de proyección para un mapeo inequívoco.

manish
fuente
1
No funciona, error de disparo:Caused by: java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate class [SurveyAnswerReport] [select new SurveyAnswerReport(v.answer,count(v.id)) from com.furniturepool.domain.Survey v group by v.answer] at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1750) at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677) at org.hibernate.jpa.spi.AbstractEnti..........
Pranav C Balan
¿Qué es esto SurveyAnswerReport en su salida? Supongo que lo reemplazó SurveyAnswerStatistics con su propia clase SurveyAnswerReport. Debe especificar el nombre de la clase completamente calificado.
Bunti
8
La clase de bean debe estar completamente calificada, es decir, incluir el nombre completo del paquete. Algo como com.domain.dto.SurveyAnswerReport.
manish
2
Obtuve 'java.lang.IllegalArgumentException: PersistentEntity no debe ser nulo' cuando intento devolver el tipo personalizado de mi JpaRepository? ¿Me perdí alguna configuración?
marioosh
1
Mientras se usa la excepción de consulta nativa, dice: la excepción anidada es java.lang.IllegalArgumentException: No es un tipo administrado: clase ... ¿Por qué debería ocurrir esto?
Mikheil Zhghenti
20

Esta consulta SQL devolvería la Lista <Objeto []>.

Puedes hacerlo de esta manera:

 @RestController
 @RequestMapping("/survey")
 public class SurveyController {

   @Autowired
   private SurveyRepository surveyRepository;

     @RequestMapping(value = "/find", method =  RequestMethod.GET)
     public Map<Long,String> findSurvey(){
       List<Object[]> result = surveyRepository.findSurveyCount();
       Map<Long,String> map = null;
       if(result != null && !result.isEmpty()){
          map = new HashMap<Long,String>();
          for (Object[] object : result) {
            map.put(((Long)object[0]),object[1]);
          }
       }
     return map;
     }
 }
ozgur
fuente
1
gracias por su respuesta a esta pregunta. Fue nítido y claro
Dheeraj R
@manish Gracias, me salvaste la noche de sueño, tu método funcionó de maravilla !!!!!!!
Vineel
15

Sé que esta es una pregunta antigua y ya ha sido respondida, pero aquí hay otro enfoque:

@Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
public List<?> findSurveyCount();
rena
fuente
Me gusta tu respuesta porque no me obliga a crear una nueva clase o interfaz. Funcionó para mí.
Yuri Hassle Araújo
Funciona bien, pero prefiero el uso de Map en los genéricos en lugar de?, Ya que Map nos permitirá acceder a ellos como clave (0) y valor (1)
Samim Aftab Ahmed
10

Usando interfaces puede obtener un código más simple. No es necesario crear y llamar manualmente a los constructores

Paso 1 : Declare intefrace con los campos obligatorios:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Paso 2 : seleccione columnas con el mismo nombre que el captador en la interfaz y devuelva la interfaz desde el método del repositorio:

public interface SurveyRepository extends CrudRepository<Survey, Long> {

    @Query("select v.answer as answer, count(v) as cnt " +
           "from Survey v " +
           "group by v.answer")
    List<SurveyAnswerStatistics> findSurveyCount();

}
Nick Savenia
fuente
Desafortunadamente, las proyecciones no se pueden usar como objetos DTO desde la perspectiva de la GUI. Si quisiera reutilizar los DTO para el envío de formularios, no podría hacerlo. Aún necesitaría un bean regular separado con getters / setters. Entonces no es una buena solución.
gen b.
Además, falta la clase de encuesta
Mikheil Zhghenti
6

defina una clase personalizada de pojo, digamos sureveyQueryAnalytics y almacene el valor devuelto por la consulta en su clase personalizada de pojo

@Query(value = "select new com.xxx.xxx.class.SureveyQueryAnalytics(s.answer, count(sv)) from Survey s group by s.answer")
List<SureveyQueryAnalytics> calculateSurveyCount();
TanvirChowdhury
fuente
1
La solución es mejor O usa la proyección en el documento oficial.
Ninja
3

No me gustan los nombres de tipo java en las cadenas de consulta y los manejo con un constructor específico. Spring JPA llama implícitamente al constructor con el resultado de la consulta en el parámetro HashMap:

@Getter
public class SurveyAnswerStatistics {
  public static final String PROP_ANSWER = "answer";
  public static final String PROP_CNT = "cnt";

  private String answer;
  private Long   cnt;

  public SurveyAnswerStatistics(HashMap<String, Object> values) {
    this.answer = (String) values.get(PROP_ANSWER);
    this.count  = (Long) values.get(PROP_CNT);
  }
}

@Query("SELECT v.answer as "+PROP_ANSWER+", count(v) as "+PROP_CNT+" FROM  Survey v GROUP BY v.answer")
List<SurveyAnswerStatistics> findSurveyCount();

El código necesita Lombok para resolver @Getter

dwe
fuente
@Getter muestra un error antes de ejecutar el código, ya que no es para el tipo de objeto
user666
Se necesita Lombok. Acabo de agregar una nota a pie de página al código.
dwe
1

Acabo de resolver este problema:

  • Las proyecciones basadas en clases no funcionan con query native ( @Query(value = "SELECT ...", nativeQuery = true)), por lo que recomiendo definir DTO personalizado mediante la interfaz.
  • Antes de usar DTO debe verificar que la consulta sea sintácticamente correcta o no
Yosra ADDALI
fuente
1

Utilicé DTO (interfaz) personalizado para asignar una consulta nativa a: el enfoque más flexible y seguro para la refactorización.

El problema que tuve con esto es que, sorprendentemente, el orden de los campos en la interfaz y las columnas en la consulta es importante. Lo hice funcionar ordenando los captadores de interfaz alfabéticamente y luego ordenando las columnas en la consulta de la misma manera.

adlerer
fuente
0
@Repository
public interface ExpenseRepo extends JpaRepository<Expense,Long> {
    List<Expense> findByCategoryId(Long categoryId);

    @Query(value = "select category.name,SUM(expense.amount) from expense JOIN category ON expense.category_id=category.id GROUP BY expense.category_id",nativeQuery = true)
    List<?> getAmountByCategory();

}

El código anterior funcionó para mí.

Senthuran
fuente