Як повернути спеціальний об'єкт із запиту Spring Data JPA GROUP BY


115

Я розробляю додаток Spring Boot із Spring Data JPA. Я використовую користувальницький запит JPQL, щоб згрупувати якесь поле і отримати підрахунок. Далі йде мій метод сховища.

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

Це працює, і результат отримується наступним чином:

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

Я хотів би отримати щось подібне:

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

Як я можу цього досягти?

Відповіді:


250

Рішення для запитів JPQL

Це підтримується для запитів JPQL в специфікації JPA .

Крок 1 : Оголосіть простий клас квасолі

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

Крок 2 : Повернення екземплярів біна з методу сховища

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

Важливі примітки

  1. Не забудьте вказати повністю кваліфікований шлях до класу бобів, включаючи назву пакета. Наприклад, якщо клас bean називається MyBeanі він знаходиться в пакеті com.path.to, буде повністю кваліфікований шлях до квасоліcom.path.to.MyBean . Просто надання MyBeanне буде працювати (якщо тільки клас бобів не знаходиться в пакеті за замовчуванням).
  2. Обов’язково зателефонуйте конструктору класу bean за допомогою newключового слова.SELECT new com.path.to.MyBean(...)буде працювати, тоді як SELECT com.path.to.MyBean(...)не буде.
  3. Переконайтеся, що передаєте атрибути в точно такому ж порядку, як і очікувалося в конструкторі bean. Спроба передавати атрибути в іншому порядку призведе до виключення.
  4. Переконайтеся, що запит є дійсним запитом JPA, тобто це не нативний запит. @Query("SELECT ..."), або @Query(value = "SELECT ..."), або @Query(value = "SELECT ...", nativeQuery = false)буде працювати, тоді як @Query(value = "SELECT ...", nativeQuery = true)не буде працювати. Це відбувається тому, що власні запити передаються без змін постачальнику JPA і виконуються проти базових RDBMS як таких. Оскільки newі com.path.to.MyBeanне є дійсними ключовими словами SQL, RDBMS викидає виняток.

Рішення для власних запитів

Як зазначалося вище, new ...синтаксис є механізмом, підтримуваним JPA, і працює з усіма постачальниками JPA. Однак якщо сам запит не є запитом JPA, тобто це нативний запит, new ...синтаксис не буде працювати, оскільки запит передається безпосередньо базовій RDBMS, яка не розумієnew ключове слово, оскільки він не є частиною стандарт SQL.

У подібних ситуаціях класи бобів потрібно замінити інтерфейсами Spring Data Projection .

Крок 1. Оголосіть інтерфейс проекції

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Крок 2 : Повернення проектованих властивостей із запиту

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

Використовуйте ASключове слово SQL для відображення полів результатів для властивостей проекції для однозначного відображення.


1
Це не працює, помилка стрільби: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

Що це SurveyAnswerReport у ваших результатах? Я припускаю, що ви замінили SurveyAnswerStatistics свій клас SurveyAnswerReport. Потрібно вказати повністю кваліфіковане ім’я класу.
Банті

8
Клас квасолі повинен бути повністю кваліфікованим, тобто містити повну назву пакета. Щось подібне com.domain.dto.SurveyAnswerReport.
маніш

2
Я отримав "java.lang.IllegalArgumentException: PersistentEntity не повинен бути нульовим!", Коли я намагаюся повернути спеціальний тип з мого JpaRepository? Якусь конфігурацію я пропустив?
Маріоош

1
Під час використання виняткових запитів винятків сказано: вкладений виняток - java.lang.IllegalArgumentException: Не керований тип: клас ... Чому з цим слід пощастити?
Міхеіл Жхенті

20

Цей список повернення запитів SQL <Об'єкт []> буде.

Ви можете це зробити так:

 @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
дякую за вашу відповідь на це питання. Це було чітко і ясно
Dheeraj R

@manish Дякую, що ти врятував мій нічний сон, твій метод спрацював як шарм !!!!!!!
Vineel

15

Я знаю, що це старе питання, і на нього вже відповіли, але ось інший підхід:

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

Мені подобається ваша відповідь, оскільки це не змушує мене створити новий клас чи інтерфейс. Це працювало для мене.
Юрій

Добре працює, але я віддаю перевагу використанню Map у дженериках замість?, Оскільки Map дозволить нам отримати доступ до них як ключ (0) та значення (1)
Самім Афтаб Ахмед

10

За допомогою інтерфейсів можна отримати простіший код. Не потрібно створювати та вручну викликати конструктори

Крок 1 : Оголосіть інтеграл із необхідними полями:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Крок 2 : Виберіть стовпці з тим самим іменем, як getter в інтерфейсі, і поверніть intefrace з методу сховища:

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

}

На жаль, проекції не можна використовувати як об'єкти DTO з точки зору GUI. Якщо ви хочете повторно використовувати DTO для подання форми, ви не змогли б. Вам все одно знадобиться окрема звичайна квасоля з геттерами / сетерами. Тож це не гарне рішення.
ген b.

Також відсутній клас опитування
Міхеіл Жгенті

6

визначте спеціальний клас pojo, скажіть sureveyQueryAnalytics, і збережіть запит, що повертається, у вашому користувальницькому класі pojo

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

1
Рішення краще. Або використовувати проекцію в офіційному документі.
Ніндзя

3

Мені не подобаються імена типу java в рядках запитів і обробляти їх конкретним конструктором. Spring JPA неявно викликає конструктор з результатом запиту в параметрі 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();

Код потрібен Lombok для вирішення @Getter


@Getter показує помилку перед запуском коду, оскільки його немає для типу об’єкта
user666

Ломбок потрібен. Просто додав виноску до коду.
dwe

1

Я щойно вирішив цю проблему:

  • Проекти на основі класу не працюють з натиском native ( @Query(value = "SELECT ...", nativeQuery = true)), тому я рекомендую визначити спеціальний DTO за допомогою інтерфейсу.
  • Перед використанням DTO слід перевірити запит синтаксично правильним чи ні

1

Я використовував користувальницький DTO (інтерфейс), щоб скласти власний запит на - найбільш гнучкий підхід і безпечний для рефакторингу.

Проблема у мене була з цим - те, що дивно, порядок полів в інтерфейсі та стовпців у запиті має значення. У мене це працює, замовляючи інтерфейси для отримання інтерфейсу в алфавітному порядку, а потім впорядковуючи стовпці в запиті таким же чином.


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

}

Наведений вище код працював для мене.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.