web-dev-qa-db-ja.com

Spring Data JPAのGROUP BYクエリからカスタムオブジェクトを返す方法

私はSpring Data 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" }
]

どうすればこれを達成できますか?

69
Pranav C Balan

JPQLクエリのためのソリューション

これは JPA仕様 内のJPQLクエリでサポートされています。

ステップ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. パッケージ名を含む、Beanクラスへの絶対パスを必ず指定してください。たとえば、BeanクラスがMyBeanという名前でパッケージcom.path.to内にある場合、Beanへの完全修飾パスはcom.path.to.MyBeanになります。 MyBeanを指定しただけでは機能しません(Beanクラスがデフォルトパッケージに含まれていない場合)。
  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クエリではない場合、つまりネイティブクエリである場合、クエリは基礎となるRDBMSに直接渡されるため、newキーワードを理解できないため、new ...構文は機能しませんこれは標準SQLの一部ではないためです。

このような状況では、Beanクラスを 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();
}

SQL ASキーワードを使用して、明確なマッピングのために結果フィールドを投影プロパティにマッピングします。

162
manish

この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;
     }
 }
16
ozgur

私はこれが古い質問であり、それはすでに答えられていることを知っています、しかしここにもう一つのアプローチがあります:

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

インターフェースを使用すると、より簡単なコードを入手できます。コンストラクタを作成して手動で呼び出す必要はありません

ステップ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();

}
6
Nick Savenia

カスタム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();
4
TanvirChowdhury

私はクエリ文字列の中で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を必要とします

2
dwe

私はちょうどこの問題を解決しました:

  • クラスベースの射影はネイティブクエリ(@Query(value = "SELECT ...", nativeQuery = true)では動作しませんので、インターフェイスを使用してカスタムDTOを定義することをお勧めします。
  • DTOを使用する前に、クエリが構文的に正しいかどうかを検証する必要があります。
0
Yosra ADDALI