Як повернути спеціальний об'єкт із запиту 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.