Como retornar um objeto personalizado de uma consulta Spring Data JPA GROUP BY

115

Estou desenvolvendo um aplicativo Spring Boot com Spring Data JPA. Estou usando uma consulta JPQL personalizada para agrupar por algum campo e obter a contagem. A seguir está o meu método de repositório.

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

Está funcionando e o resultado é obtido da seguinte maneira:

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

Eu gostaria de obter algo assim:

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

Como posso conseguir isso?

Pranav C Balan
fonte

Respostas:

251

Solução para consultas JPQL

Isso é suportado para consultas JPQL na especificação JPA .

Etapa 1 : declarar uma classe de bean simples

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;
  }
}

Etapa 2 : retornar instâncias de bean do método de repositório

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();
}

Anotações importantes

  1. Certifique-se de fornecer o caminho totalmente qualificado para a classe do bean, incluindo o nome do pacote. Por exemplo, se a classe do bean for chamada MyBeane estiver no pacote com.path.to, o caminho totalmente qualificado para o bean será com.path.to.MyBean. Simplesmente fornecer MyBeannão funcionará (a menos que a classe do bean esteja no pacote padrão).
  2. Certifique-se de chamar o construtor da classe do bean usando a newpalavra - chave. SELECT new com.path.to.MyBean(...)vai funcionar, SELECT com.path.to.MyBean(...)mas não.
  3. Certifique-se de passar os atributos exatamente na mesma ordem esperada no construtor do bean. Tentar passar atributos em uma ordem diferente levará a uma exceção.
  4. Certifique-se de que a consulta seja uma consulta JPA válida, ou seja, não é uma consulta nativa. @Query("SELECT ..."), ou @Query(value = "SELECT ..."), ou @Query(value = "SELECT ...", nativeQuery = false)funcionará, enquanto @Query(value = "SELECT ...", nativeQuery = true)não funcionará. Isso ocorre porque as consultas nativas são passadas sem modificações para o provedor JPA e são executadas no RDBMS subjacente como tal. Como newe com.path.to.MyBeannão são palavras-chave SQL válidas, o RDBMS lança uma exceção.

Solução para consultas nativas

Conforme observado acima, a new ...sintaxe é um mecanismo suportado por JPA e funciona com todos os provedores JPA. No entanto, se a consulta em si não for uma consulta JPA, ou seja, é uma consulta nativa, a new ...sintaxe não funcionará, pois a consulta é passada diretamente para o RDBMS subjacente, que não entende a newpalavra-chave, pois não faz parte de o padrão SQL.

Em situações como essas, as classes de bean precisam ser substituídas por interfaces Spring Data Projection .

Etapa 1 : declarar uma interface de projeção

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Etapa 2 : Retorne as propriedades projetadas da 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();
}

Use a ASpalavra-chave SQL para mapear campos de resultado para propriedades de projeção para mapeamento inequívoco.

manish
fonte
1
Não está funcionando, erro 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
O que é isso SurveyAnswerReport em sua saída. Presumo que você substituiu SurveyAnswerStatistics por sua própria classe SurveyAnswerReport. Você precisa especificar o nome da classe totalmente qualificado.
Bunti
8
A classe do bean deve ser totalmente qualificada, ou seja, incluir o nome completo do pacote. Algo parecido com.domain.dto.SurveyAnswerReport.
manish
2
Recebi 'java.lang.IllegalArgumentException: PersistentEntity não deve ser nulo! `Quando tento retornar o tipo personalizado do meu JpaRepository? Esqueci alguma configuração?
marioosh
1
Ao usar a exceção de consulta nativa diz: a exceção aninhada é java.lang.IllegalArgumentException: Não é um tipo gerenciado: classe ... Por que isso deve acontecer?
Mikheil Zhghenti
20

Esta consulta SQL retorna List <Object []>.

Você pode fazer desta forma:

 @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
fonte
1
obrigado pela sua resposta a esta pergunta. Estava nítido e claro
Dheeraj R
@manish Obrigado você salvou minha noite de sono, seu método funcionou perfeitamente !!!!!!!
Vineel
15

Eu sei que esta é uma pergunta antiga e já foi respondida, mas aqui está outra abordagem:

@Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
public List<?> findSurveyCount();
rena
fonte
Gosto da sua resposta porque não me obriga a criar uma nova classe ou interface. Funcionou para mim
Yuri Hassle Araújo
Funciona bem, mas eu prefiro o uso de Map nos genéricos ao invés de?, Já que o Map nos permitirá acessá-los como chave (0) e valor (1)
Samim Aftab Ahmed
10

Usando interfaces, você pode obter um código mais simples. Não há necessidade de criar e chamar construtores manualmente

Etapa 1 : Declare a integração com os campos obrigatórios:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Etapa 2 : Selecione colunas com o mesmo nome de getter na interface e retorne a intefrace do método de repositório:

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
fonte
Infelizmente, as projeções não podem ser usadas como objetos DTO da perspectiva da GUI. Se você quisesse reutilizar DTOs para envio de formulário, não seria possível. Você ainda precisará de um bean regular separado com getters / setters. Portanto, não é uma boa solução.
gene b.
Além disso, está faltando a Survey Class
Mikheil Zhghenti
6

definir uma classe Pojo customizada, digamos sureveyQueryAnalytics, e armazenar o valor retornado da consulta em sua classe Pojo customizada

@Query(value = "select new com.xxx.xxx.class.SureveyQueryAnalytics(s.answer, count(sv)) from Survey s group by s.answer")
List<SureveyQueryAnalytics> calculateSurveyCount();
TanvirChowdhury
fonte
1
A solução é melhor.Ou use a projeção no documento oficial.
Ninja
3

Não gosto de nomes de tipo java em strings de consulta e manuseio-os com um construtor específico. O Spring JPA chama implicitamente o construtor com o resultado da consulta no 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();

O código precisa do Lombok para resolver @Getter

dwe
fonte
@Getter está mostrando um erro antes de executar o código, pois não é para o tipo de objeto
user666
O Lombok é necessário. Acabei de adicionar uma nota de rodapé ao código.
dwe
1

Acabei de resolver este problema:

  • Projeções baseadas em classe não funcionam com query native ( @Query(value = "SELECT ...", nativeQuery = true)) então eu recomendo definir DTO personalizado usando interface.
  • Antes de usar o DTO deve-se verificar se a consulta está sintaticamente correta ou não
Yosra ADDALI
fonte
1

Usei DTO (interface) customizado para mapear uma consulta nativa - a abordagem mais flexível e segura para refatoração.

O problema que tive com isso - que, surpreendentemente, a ordem dos campos na interface e as colunas na consulta são importantes. Eu fiz isso funcionar ordenando os getters de interface em ordem alfabética e, em seguida, ordenando as colunas na consulta da mesma maneira.

adlerer
fonte
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();

}

O código acima funcionou para mim.

Senthuran
fonte