Как вернуть настраиваемый объект из запроса 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" }
]

Как я могу этого добиться?

Пранав Ч. Балан
источник

Ответы:

253

Решение для запросов JPQL

Это поддерживается для запросов JPQL в спецификации JPA .

Шаг 1. Объявите простой класс 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;
  }
}

Шаг 2. Верните экземпляры bean-компонентов из метода репозитория

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. Обязательно укажите полный путь к классу компонента, включая имя пакета. Например, если вызывается класс компонента, MyBeanи он находится в пакете com.path.to, полный путь к компоненту будет com.path.to.MyBean. Простое предоставление MyBeanне будет работать (если класс компонента не находится в пакете по умолчанию).
  2. Обязательно вызовите конструктор класса компонента с помощью 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 без изменений и выполняются для базовой СУБД как таковой. Поскольку newи com.path.to.MyBeanне являются допустимыми ключевыми словами SQL, СУБД генерирует исключение.

Решение для собственных запросов

Как отмечалось выше, new ...синтаксис является механизмом, поддерживаемым JPA, и работает со всеми поставщиками JPA. Однако, если сам запрос не является запросом JPA, то есть это собственный запрос, new ...синтаксис не будет работать, поскольку запрос передается непосредственно в базовую СУБД, которая не понимает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. Вам необходимо указать полное имя класса.
Bunti
8
Класс bean-компонента должен быть полностью определен, то есть включать полное имя пакета. Что-то вроде com.domain.dto.SurveyAnswerReport.
Manish
2
У меня есть исключение java.lang.IllegalArgumentException: PersistentEntity не должно быть нулевым! `, Когда я пытаюсь вернуть настраиваемый тип из моего JpaRepository? Я пропустил какую-то конфигурацию?
Marioosh
1
При использовании собственного исключения запроса говорится: вложенное исключение java.lang.IllegalArgumentException: не управляемый тип: класс ... Почему это должно происходить?
Михаил Жгенти
20

Этот SQL-запрос возвращает List <Object []>.

Сделать это можно так:

 @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 01
15

Я знаю, что это старый вопрос, и на него уже был дан ответ, но вот другой подход:

@Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
public List<?> findSurveyCount();
Рена
источник
Мне нравится ваш ответ, потому что он не заставляет меня создавать новый класс или интерфейс. У меня это сработало.
Юрий Хассле Араужо
Работает нормально, но я предпочитаю использовать Map в универсальных шаблонах вместо?,
Самим Афтаб Ахмед
10

Используя интерфейсы, вы можете получить более простой код. Нет необходимости создавать и вызывать конструкторы вручную

Шаг 1. Объявите intefrace с обязательными полями:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Шаг 2 : Выберите столбцы с тем же именем, что и геттер в интерфейсе, и верните 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 с точки зрения графического интерфейса. Если бы вы хотели повторно использовать DTO для отправки формы, вы бы не смогли. Вам все равно понадобится отдельный обычный bean-компонент с геттерами / сеттерами. Так что это не лучшее решение.
ген 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
Решение лучше. Или используйте проекцию в официальном документе.
Ninja
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

dwe
источник
@Getter показывает ошибку перед запуском кода, поскольку он не для типа объекта
user666
Ломбок нужен. Просто добавила сноску к коду.
dwe
1

Я только что решил эту проблему:

  • Проекции на основе классов не работают с собственными запросами (@Query(value = "SELECT ...", nativeQuery = true )), поэтому я рекомендую определять собственный DTO с помощью интерфейса.
  • Перед использованием DTO следует проверить синтаксически правильный запрос или нет
Йосра АДДАЛИ
источник
1

Я использовал настраиваемый DTO (интерфейс) для сопоставления собственного запроса - наиболее гибкий подход и безопасный для рефакторинга.

Проблема, с которой я столкнулся, - это удивительно, что порядок полей в интерфейсе и столбцов в запросе имеет значение. Я заставил его работать, упорядочив геттеры интерфейса в алфавитном порядке, а затем таким же образом упорядочив столбцы в запросе.

adlerer
источник
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();

}

Приведенный выше код работал у меня.

Senthuran
источник