Come restituire un oggetto personalizzato da una query Spring Data JPA GROUP BY


115

Sto sviluppando un'applicazione Spring Boot con Spring Data JPA. Sto usando una query JPQL personalizzata per raggruppare in base a un campo e ottenere il conteggio. Di seguito è riportato il mio metodo di repository.

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

Funziona e il risultato si ottiene come segue:

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

Vorrei ottenere qualcosa del genere:

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

Come posso raggiungere questo obiettivo?

Risposte:


249

Soluzione per query JPQL

Questo è supportato per le query JPQL all'interno della specifica JPA .

Passaggio 1 : dichiarare una semplice classe di bean

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

Passaggio 2 : restituire le istanze del bean dal metodo del repository

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

Note importanti

  1. Assicurati di fornire il percorso completo alla classe bean, incluso il nome del pacchetto. Ad esempio, se la classe bean viene chiamata MyBeaned è nel pacchetto com.path.to, il percorso completo del bean sarà com.path.to.MyBean. La semplice fornitura MyBeannon funzionerà (a meno che la classe bean non sia nel pacchetto predefinito).
  2. Assicurati di chiamare il costruttore della classe bean usando la newparola chiave. SELECT new com.path.to.MyBean(...)funzionerà, mentre SELECT com.path.to.MyBean(...)no.
  3. Assicurati di passare gli attributi esattamente nello stesso ordine di quello previsto nel costruttore del bean. Il tentativo di passare attributi in un ordine diverso porterà a un'eccezione.
  4. Assicurati che la query sia una query JPA valida, ovvero non è una query nativa. @Query("SELECT ..."), o @Query(value = "SELECT ..."), o @Query(value = "SELECT ...", nativeQuery = false)funzionerà, mentre @Query(value = "SELECT ...", nativeQuery = true)non funzionerà. Questo perché le query native vengono passate senza modifiche al provider JPA e vengono eseguite sull'RDBMS sottostante in quanto tale. Poiché newe com.path.to.MyBeannon sono parole chiave SQL valide, RDBMS genera un'eccezione.

Soluzione per query native

Come notato sopra, la new ...sintassi è un meccanismo supportato da JPA e funziona con tutti i fornitori di JPA. Tuttavia, se la query stessa non è una query JPA, ovvero è una query nativa, la new ...sintassi non funzionerà poiché la query viene passata direttamente all'RDBMS sottostante, che non comprende la newparola chiave poiché non fa parte di lo standard SQL.

In situazioni come queste, le classi di bean devono essere sostituite con le interfacce Spring Data Projection .

Passaggio 1 : dichiarare un'interfaccia di proiezione

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Passaggio 2 : restituire le proprietà proiettate dalla query

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

Utilizzare la ASparola chiave SQL per mappare i campi dei risultati alle proprietà di proiezione per una mappatura univoca.


1
Non funziona, errore di sparo: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

Cos'è questo SurveyAnswerReport nel tuo output. Presumo tu abbia sostituito SurveyAnswerStatistics con la tua classe SurveyAnswerReport. È necessario specificare il nome completo della classe.
Bunti

8
La classe bean deve essere completamente qualificata, ovvero includere il nome completo del pacchetto. Qualcosa di simile com.domain.dto.SurveyAnswerReport.
manish

2
Ho ricevuto "java.lang.IllegalArgumentException: PersistentEntity non deve essere nullo!" Quando provo a restituire il tipo personalizzato dal mio JpaRepository? Mi sono perso qualche configurazione?
marioosh

1
Durante l'utilizzo dell'eccezione di query nativa dice: l'eccezione annidata è java.lang.IllegalArgumentException: Non è un tipo gestito: class ... Perché dovrebbe essere accaduto?
Mikheil Zhghenti

20

Questa query SQL restituisce List <Object []>.

Puoi farlo in questo modo:

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

1
grazie per la tua risposta a questa domanda. Era nitido e chiaro
Dheeraj R

@manish Grazie mi hai salvato la notte di sonno, il tuo metodo ha funzionato a meraviglia !!!!!!!
Vineel

15

So che questa è una vecchia domanda ed è già stata data una risposta, ma ecco un altro approccio:

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

Mi piace la tua risposta perché non mi obbliga a creare una nuova classe o interfaccia. Ha funzionato per me.
Yuri Hassle Araújo

Funziona bene ma preferisco l'uso di Map nei generici invece di?, Poiché Map ci consentirà di accedervi come chiave (0) e valore (1)
Samim Aftab Ahmed

10

Usando le interfacce puoi ottenere un codice più semplice. Non è necessario creare e chiamare manualmente i costruttori

Passaggio 1 : dichiara intefrace con i campi obbligatori:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Passaggio 2 : selezionare le colonne con lo stesso nome di getter nell'interfaccia e restituire intefrace dal metodo del repository:

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

}

Sfortunatamente le proiezioni non possono essere utilizzate come oggetti DTO dal punto di vista della GUI. Se volessi riutilizzare i DTO per l'invio di moduli non saresti in grado di farlo. Avresti comunque bisogno di un bean normale separato con getter / setter. Quindi non è una buona soluzione.
gene b.

Inoltre, manca la Survey Class
Mikheil Zhghenti il

6

definire una classe pojo personalizzata dire sureveyQueryAnalytics e memorizzare il valore restituito dalla query nella classe pojo personalizzata

@Query(value = "select new com.xxx.xxx.class.SureveyQueryAnalytics(s.answer, count(sv)) from Survey s group by s.answer")
List<SureveyQueryAnalytics> calculateSurveyCount();

1
La soluzione è migliore Oppure usa la proiezione nel documento ufficiale.
Ninja

3

Non mi piacciono i nomi dei tipi Java nelle stringhe di query e li gestisco con un costruttore specifico. Spring JPA chiama implicitamente il costruttore con il risultato della query nel parametro 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();

Il codice ha bisogno di Lombok per risolvere @Getter


@Getter mostra un errore prima di eseguire il codice perché non è per il tipo di oggetto
user666

Serve Lombok. Ho appena aggiunto una nota a piè di pagina al codice.
DWE

1

Ho appena risolto questo problema:

  • Le proiezioni basate sulla classe non funzionano con query native ( @Query(value = "SELECT ...", nativeQuery = true)), quindi consiglio di definire DTO personalizzato utilizzando l'interfaccia.
  • Prima di utilizzare DTO dovrebbe verificare la query sintaticamente corretta o meno

1

Ho usato DTO (interfaccia) personalizzato per mappare una query nativa a: l'approccio più flessibile e sicuro per il refactoring.

Il problema che ho avuto con questo - che sorprendentemente, l'ordine dei campi nell'interfaccia e le colonne nella query sono importanti. Ho funzionato ordinando i getter dell'interfaccia in ordine alfabetico e quindi ordinando le colonne nella query allo stesso modo.


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

}

Il codice sopra ha funzionato per me.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.