Jak zwrócić obiekt niestandardowy z zapytania Spring Data JPA GROUP BY

115

Rozwijam aplikację Spring Boot z Spring Data JPA. Używam niestandardowego zapytania JPQL, aby pogrupować według jakiegoś pola i uzyskać liczbę. Poniżej znajduje się moja metoda repozytorium.

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

Działa, a wynik jest następujący:

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

Chciałbym dostać coś takiego:

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

Jak mogę to osiągnąć?

Pranav C Balan
źródło

Odpowiedzi:

252

Rozwiązanie dla zapytań JPQL

Jest to obsługiwane w przypadku zapytań JPQL w ramach specyfikacji JPA .

Krok 1 : Zadeklaruj prostą klasę fasoli

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

Krok 2 : Zwróć instancje bean z metody repozytorium

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

Ważne notatki

  1. Upewnij się, że podano w pełni kwalifikowaną ścieżkę do klasy bean, w tym nazwę pakietu. Na przykład, jeśli wywoływana jest klasa bean MyBeani znajduje się ona w pakiecie com.path.to, w pełni kwalifikowana ścieżka do komponentu bean będzie com.path.to.MyBean. Samo podanie MyBeannie zadziała (chyba że klasa bean znajduje się w domyślnym pakiecie).
  2. Pamiętaj, aby wywołać konstruktor klasy bean przy użyciu newsłowa kluczowego. SELECT new com.path.to.MyBean(...)zadziała, ale SELECT com.path.to.MyBean(...)nie zadziała.
  3. Upewnij się, że atrybuty są przekazywane dokładnie w takiej samej kolejności, jak oczekiwano w konstruktorze bean. Próba przekazania atrybutów w innej kolejności doprowadzi do wyjątku.
  4. Upewnij się, że zapytanie jest prawidłowym zapytaniem JPA, czyli nie jest zapytaniem natywnym. @Query("SELECT ..."), lub @Query(value = "SELECT ..."), lub @Query(value = "SELECT ...", nativeQuery = false)będzie działać, ale @Query(value = "SELECT ...", nativeQuery = true)nie będzie działać. Dzieje się tak, ponieważ natywne zapytania są przekazywane bez modyfikacji do dostawcy JPA i są wykonywane względem bazowego RDBMS jako takiego. Ponieważ newi com.path.to.MyBeannie są poprawnymi słowami kluczowymi SQL, RDBMS zgłasza wyjątek.

Rozwiązanie dla zapytań natywnych

Jak wspomniano powyżej, new ...składnia jest mechanizmem obsługiwanym przez JPA i działa ze wszystkimi dostawcami JPA. Jeśli jednak samo zapytanie nie jest zapytaniem JPA, to znaczy jest zapytaniem natywnym, new ...składnia nie będzie działać, ponieważ zapytanie jest przekazywane bezpośrednio do bazowego systemu RDBMS, który nie rozumie newsłowa kluczowego, ponieważ nie jest częścią standard SQL.

W takich sytuacjach klasy bean należy zastąpić interfejsami Spring Data Projection .

Krok 1 : Zadeklaruj interfejs projekcji

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Krok 2 : Zwróć przewidywane właściwości z zapytania

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

Użyj ASsłowa kluczowego SQL, aby odwzorować pola wynikowe na właściwości rzutowania w celu jednoznacznego odwzorowania.

manish
źródło
1
Nie działa, błąd odpalenia: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
Co to jest SurveyAnswerReport w twoim wyniku. Zakładam, że zastąpiłeś SurveyAnswerStatistics własną klasą SurveyAnswerReport. Musisz określić w pełni kwalifikowaną nazwę klasy.
Bunti
8
Klasa bean musi być w pełni kwalifikowana, to znaczy zawierać pełną nazwę pakietu. Coś jak com.domain.dto.SurveyAnswerReport.
manisz
2
Otrzymałem „java.lang.IllegalArgumentException: PersistentEntity nie może mieć wartości null!”, Gdy próbuję zwrócić niestandardowy typ z mojego JpaRepository? Czy brakuje mi jakiejś konfiguracji?
marioosh
1
Podczas korzystania z natywnego wyjątku zapytania mówi: zagnieżdżony wyjątek to java.lang.IllegalArgumentException: Not a managed type: class ... Dlaczego należy to zrobić?
Mikheil Zhghenti
20

To zapytanie SQL zwróci List <Object []>.

Możesz to zrobić w ten sposób:

 @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
źródło
1
dziękuję za odpowiedź na to pytanie. Było ostro i wyraźnie
Dheeraj R
@manish Dzięki zaoszczędziłeś mi sen, Twoja metoda zadziałała jak urok !!!!!!!
Vineel
15

Wiem, że to stare pytanie i już udzielono na nie odpowiedzi, ale oto inne podejście:

@Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
public List<?> findSurveyCount();
rena
źródło
Podoba mi się twoja odpowiedź, ponieważ nie zmusza mnie to do tworzenia nowej klasy lub interfejsu. U mnie to zadziałało.
Yuri Hassle Araújo
Działa dobrze, ale wolę używać Map w typach ogólnych zamiast?, Ponieważ Mapa pozwoli nam uzyskać do nich dostęp jako klucz (0) i wartość (1)
Samim Aftab Ahmed
10

Korzystając z interfejsów, możesz uzyskać prostszy kod. Nie ma potrzeby tworzenia i ręcznego wywoływania konstruktorów

Krok 1 : Zadeklaruj interakcję z wymaganymi polami:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Krok 2 : Wybierz kolumny o tej samej nazwie co getter w interfejsie i zwróć intefrace z metody repozytorium:

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
źródło
Niestety projekcji nie można używać jako obiektów DTO z perspektywy GUI. Gdybyś chciał ponownie wykorzystać DTO do przesyłania formularzy, nie byłbyś w stanie. Nadal potrzebujesz oddzielnej zwykłej fasoli z getterami / ustawiaczami. Więc to nie jest dobre rozwiązanie.
gen b.
Brakuje również klasy Survey
Mikheil Zhghenti
6

zdefiniuj niestandardową klasę pojo, powiedz sureveyQueryAnalytics i zapisz wartość zwróconą przez zapytanie w swojej niestandardowej klasie 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
źródło
1
Rozwiązanie jest lepsze lub skorzystaj z projekcji w oficjalnym dokumencie.
Ninja
3

Nie lubię nazw typów Java w ciągach zapytań i obsługuję je za pomocą określonego konstruktora. Spring JPA niejawnie wywołuje konstruktor z wynikiem zapytania w parametrze 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();

Kod potrzebuje Lomboka do rozpoznania @Getter

dwe
źródło
@Getter wyświetla błąd przed uruchomieniem kodu, ponieważ nie jest to typ obiektu
user666
Potrzebny jest Lombok. Właśnie dodałem przypis do kodu.
dzień
1

Właśnie rozwiązałem ten problem:

  • Projekcje oparte na klasach nie działają z natywnym zapytaniem ( @Query(value = "SELECT ...", nativeQuery = true)), więc zalecam zdefiniowanie niestandardowego DTO za pomocą interfejsu.
  • Przed użyciem DTO należy sprawdzić poprawność składniową zapytania, czy nie
Yosra ADDALI
źródło
1

Użyłem niestandardowego DTO (interfejsu) do odwzorowania natywnego zapytania na - najbardziej elastyczne podejście i bezpieczne dla refaktoryzacji.

Problem, który miałem z tym - to zaskakujące, kolejność pól w interfejsie i kolumny w zapytaniu mają znaczenie. Udało mi się to, porządkując alfabetycznie metody pobierające interfejs, a następnie ustawiając kolumny w zapytaniu w ten sam sposób.

adlerer
źródło
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();

}

Powyższy kod zadziałał dla mnie.

Senthuran
źródło