So geben Sie ein benutzerdefiniertes Objekt aus einer Spring Data JPA GROUP BY-Abfrage zurück


115

Ich entwickle eine Spring Boot-Anwendung mit Spring Data JPA. Ich verwende eine benutzerdefinierte JPQL-Abfrage, um nach einem Feld zu gruppieren und die Anzahl zu ermitteln. Das Folgende ist meine Repository-Methode.

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

Es funktioniert und das Ergebnis wird wie folgt erhalten:

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

Ich möchte so etwas bekommen:

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

Wie kann ich das erreichen?

Antworten:


249

Lösung für JPQL-Abfragen

Dies wird für JPQL-Abfragen innerhalb der JPA-Spezifikation unterstützt .

Schritt 1 : Deklarieren Sie eine einfache Bean-Klasse

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

Schritt 2 : Bean-Instanzen von der Repository-Methode zurückgeben

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

Wichtige Notizen

  1. Stellen Sie sicher, dass Sie den vollständig qualifizierten Pfad zur Bean-Klasse einschließlich des Paketnamens angeben. Wenn beispielsweise die Bean-Klasse aufgerufen wird MyBeanund sich im Paket befindet com.path.to, lautet der vollständig qualifizierte Pfad zur Bean com.path.to.MyBean. Einfach die Bereitstellung MyBeannicht funktionieren (es sei denn , die Bean - Klasse im Standardpaket ist).
  2. Stellen Sie sicher, dass Sie den Bean-Klassenkonstruktor mit dem newSchlüsselwort aufrufen . SELECT new com.path.to.MyBean(...)wird funktionieren, während SELECT com.path.to.MyBean(...)nicht.
  3. Stellen Sie sicher, dass die Attribute in genau der Reihenfolge übergeben werden, die im Bean-Konstruktor erwartet wird. Der Versuch, Attribute in einer anderen Reihenfolge zu übergeben, führt zu einer Ausnahme.
  4. Stellen Sie sicher, dass die Abfrage eine gültige JPA-Abfrage ist, dh keine native Abfrage. @Query("SELECT ..."), oder @Query(value = "SELECT ..."), oder @Query(value = "SELECT ...", nativeQuery = false)wird funktionieren, während @Query(value = "SELECT ...", nativeQuery = true)wird nicht funktionieren. Dies liegt daran, dass native Abfragen ohne Änderungen an den JPA-Anbieter übergeben und für das zugrunde liegende RDBMS als solches ausgeführt werden. Da newund com.path.to.MyBeansind keine gültigen SQL-Schlüsselwörter, löst das RDBMS dann eine Ausnahme aus.

Lösung für native Abfragen

Wie oben erwähnt, ist die new ...Syntax ein JPA-unterstützter Mechanismus und funktioniert mit allen JPA-Anbietern. Wenn es sich bei der Abfrage selbst jedoch nicht um eine JPA-Abfrage handelt, dh um eine native Abfrage, new ...funktioniert die Syntax nicht, da die Abfrage direkt an das zugrunde liegende RDBMS weitergeleitet wird, das das newSchlüsselwort nicht versteht, da es nicht Teil von ist der SQL-Standard.

In solchen Situationen müssen Bean-Klassen durch Spring Data Projection- Schnittstellen ersetzt werden.

Schritt 1 : Deklarieren Sie eine Projektionsschnittstelle

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Schritt 2 : Geben Sie die projizierten Eigenschaften aus der Abfrage zurück

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

Verwenden Sie das ASSchlüsselwort SQL , um Ergebnisfelder Projektionseigenschaften für eine eindeutige Zuordnung zuzuordnen.


1
Es funktioniert nicht, Feuerfehler: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

Was ist das SurveyAnswerReport in Ihrer Ausgabe. Ich nehme an, Sie haben durch SurveyAnswerStatistics Ihre eigene Klasse ersetzt SurveyAnswerReport. Sie müssen den vollständig qualifizierten Klassennamen angeben.
Bunti

8
Die Bean-Klasse muss vollständig qualifiziert sein, dh den vollständigen Paketnamen enthalten. So etwas wie com.domain.dto.SurveyAnswerReport.
Manish

2
Ich habe 'java.lang.IllegalArgumentException: PersistentEntity darf nicht null sein!', Wenn ich versuche, einen benutzerdefinierten Typ von meinem zurückzugeben JpaRepository? Ist eine Konfiguration, die ich verpasst habe?
Marioosh

1
Bei Verwendung der nativen Abfrage lautet die Ausnahme: Verschachtelte Ausnahme ist java.lang.IllegalArgumentException: Kein verwalteter Typ: Klasse ... Warum sollte dies geschehen?
Mikheil Zhghenti

20

Diese SQL-Abfrage würde List <Object []> zurückgeben.

Sie können es so machen:

 @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
Vielen Dank für Ihre Antwort auf diese Frage. Es war klar und deutlich
Dheeraj R

@manish Danke, du hast mir den Schlaf erspart, deine Methode hat wie ein Zauber gewirkt !!!!!!!
Vineel

15

Ich weiß, dass dies eine alte Frage ist und bereits beantwortet wurde, aber hier ist ein anderer Ansatz:

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

Ich mag Ihre Antwort, weil sie mich nicht zwingt, eine neue Klasse oder Schnittstelle zu erstellen. Es hat bei mir funktioniert.
Yuri Hassle Araújo

Funktioniert gut, aber ich bevorzuge die Verwendung von Map in den Generika anstelle von?, Da Map uns den Zugriff als Schlüssel (0) und Wert (1) ermöglicht
Samim Aftab Ahmed

10

Über Schnittstellen können Sie einfacheren Code erhalten. Konstruktoren müssen nicht erstellt und manuell aufgerufen werden

Schritt 1 : Deklarieren Sie intefrace mit den erforderlichen Feldern:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Schritt 2 : Wählen Sie Spalten mit demselben Namen wie Getter in der Schnittstelle aus und geben Sie intefrace von der Repository-Methode zurück:

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

}

Leider können Projektionen aus GUI-Sicht nicht als DTO-Objekte verwendet werden. Wenn Sie DTOs für die Formularübermittlung wiederverwenden möchten, können Sie dies nicht. Sie würden immer noch eine separate reguläre Bohne mit Getter / Setter benötigen. Es ist also keine gute Lösung.
Gen b.

Außerdem fehlt die
Umfrageklasse

6

Definieren Sie eine benutzerdefinierte Pojo-Klasse, sagen Sie sureveyQueryAnalytics, und speichern Sie den zurückgegebenen Abfragewert in Ihrer benutzerdefinierten Pojo-Klasse

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

1
Die Lösung ist besser. Oder verwenden Sie die Projektion im offiziellen Dokument.
Ninja

3

Ich mag keine Java-Typnamen in Abfragezeichenfolgen und behandle sie mit einem bestimmten Konstruktor. Spring JPA ruft implizit den Konstruktor mit dem Abfrageergebnis im HashMap-Parameter auf:

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

Code benötigt Lombok zum Auflösen von @Getter


@Getter zeigt einen Fehler an, bevor der Code ausgeführt wird, da er nicht für den Objekttyp gilt
user666

Lombok wird benötigt. Habe gerade eine Fußnote zum Code hinzugefügt.
dwe

1

Ich habe gerade dieses Problem gelöst:

  • Klassenbasierte Projektionen funktionieren nicht mit query native ( @Query(value = "SELECT ...", nativeQuery = true)). Ich empfehle daher, ein benutzerdefiniertes DTO über die Schnittstelle zu definieren.
  • Vor der Verwendung von DTO sollte überprüft werden, ob die Abfrage syntaktisch korrekt ist oder nicht

1

Ich habe ein benutzerdefiniertes DTO (Schnittstelle) verwendet, um eine native Abfrage zuzuordnen - dem flexibelsten Ansatz und Refactoring-sicher.

Das Problem, das ich damit hatte - dass überraschenderweise die Reihenfolge der Felder in der Schnittstelle und der Spalten in der Abfrage von Bedeutung ist. Ich habe es zum Laufen gebracht, indem ich Interface-Getter alphabetisch sortiert und dann die Spalten in der Abfrage auf die gleiche Weise sortiert habe.


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

}

Der obige Code hat bei mir funktioniert.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.