SpringData JPA GROUP BY 쿼리에서 사용자 지정 개체를 반환하는 방법


115

SpringData JPA로 Spring Boot 애플리케이션을 개발 중입니다. 사용자 지정 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 쿼리 솔루션

이것은 JPA 사양 내의 JPQL 쿼리에 대해 지원됩니다 .

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 클래스에 대한 완전한 경로를 제공해야합니다. 예를 들어, Bean 클래스가 호출 MyBean되고 package com.path.to에있는 경우 Bean에 대한 완전한 경로는입니다 com.path.to.MyBean. 단순히 제공하는 MyBean것은 작동하지 않습니다 (빈 클래스가 기본 패키지에 있지 않는 한).
  2. new키워드를 사용하여 Bean 클래스 생성자를 호출해야 합니다. 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에 대해 실행되기 때문입니다. 이후 newcom.path.to.MyBean유효한 SQL 키워드 아니다는 RDBMS는 예외가 발생합니다.

네이티브 쿼리를위한 솔루션

위에서 언급했듯이 new ...구문은 JPA 지원 메커니즘이며 모든 JPA 공급자와 함께 작동합니다. 그러나, 즉, 네이티브 쿼리가 JPA 쿼리하지 쿼리 자체 인 경우, new ...구문은 쿼리로하지 작업은 이해하지 못하는 기본 RDBMS에 직접 전달되는 것 new이 부분이 아니기 때문에 키워드 SQL 표준.

이와 같은 상황에서는 빈 클래스를 SpringData 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();
}

SQL AS키워드를 사용하여 결과 필드를 명확한 매핑을 위해 프로젝션 속성에 매핑합니다.


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 발안을

SurveyAnswerReport 출력에서 이것은 무엇입니까? 나는 당신 SurveyAnswerStatistics 이 당신 자신의 수업으로 대체되었다고 가정합니다 SurveyAnswerReport. 완전한 클래스 이름을 지정해야합니다.
Bunti

8
Bean 클래스는 완전한 패키지 이름을 포함해야합니다. 같은 것 com.domain.dto.SurveyAnswerReport.
manish

2
에서 사용자 정의 유형을 반환하려고 할 때 'java.lang.IllegalArgumentException : PersistentEntity must not be null!'이 발생했습니다 JpaRepository. 내가 놓친 일부 구성입니까?
marioosh

1
네이티브 쿼리 예외를 사용하는 동안 다음과 같이 말합니다. 중첩 된 예외는 java.lang.IllegalArgumentException입니다. 관리되는 유형이 아닙니다. class ... 왜 이것이 발생해야합니까?
Mikheil Zhghenti

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

15

나는 이것이 오래된 질문이고 이미 답변을 받았음을 알고 있지만 여기에 또 다른 접근 방식이 있습니다.

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

새 클래스 또는 인터페이스를 만들도록 강요하지 않기 때문에 귀하의 답변이 마음에 듭니다. 그것은 나를 위해 일했습니다.
Yuri Hassle Araújo

잘 작동하지만 맵이 키 (0) 및 값 (1)으로 액세스 할 수 있도록하기 때문에? 대신 제네릭에서 Map 사용을 선호합니다
Samim Aftab Ahmed

10

인터페이스를 사용하면 더 간단한 코드를 얻을 수 있습니다. 생성자를 만들고 수동으로 호출 할 필요가 없습니다.

1 단계 : 필수 필드를 사용하여 intefrace를 선언합니다.

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

}

불행히도 프로젝션은 GUI 관점에서 DTO 개체로 사용할 수 없습니다. 양식 제출에 DTO를 재사용하려는 경우 불가능합니다. getter / setter가있는 별도의 일반 빈이 여전히 필요합니다. 따라서 좋은 해결책이 아닙니다.
유전자 b.

또한 누락 된 설문 조사 클래스가 있습니다
Mikheil Zhghenti

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


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

@Getter를 해결하려면 코드에 Lombok이 필요합니다.


@Getter는 코드를 실행하기 전에 객체 유형이 아니기 때문에 오류를 표시합니다
user666

롬복이 필요합니다. 코드에 각주를 추가했습니다.
dwe

1

방금이 문제를 해결했습니다.

  • 클래스 기반 프로젝션은 쿼리 네이티브 (@Query(value = "SELECT ...", nativeQuery = true )) interface를 사용하여 사용자 지정 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();

}

위의 코드는 저에게 효과적이었습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.