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


115

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" }
]

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

回答:


249

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、それがパッケージ内にある場合、Bean com.path.toへの完全修飾パスはになります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に対して実行されるためです。newおよびcom.path.to.MyBeanは有効なSQLキーワードではないため、RDBMSは例外をスローします。

ネイティブクエリのソリューション

上記のように、new ...構文はJPAでサポートされるメカニズムであり、すべてのJPAプロバイダーで機能します。クエリ自体は、それがネイティブクエリである、つまり、JPAクエリでない場合は、new ...構文がクエリとしての仕事が直接理解していない基礎となるRDBMSに渡されません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キーワードを使用して、結果フィールドをプロジェクションプロパティにマッピングし、明確なマッピングを行います。


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 '31

これSurveyAnswerReport はあなたの出力で何ですか。あなたSurveyAnswerStatistics はあなた自身のクラスに置き換えられたと思いますSurveyAnswerReport。完全修飾クラス名を指定する必要があります。
Bunti 2016年

8
Beanクラスは完全修飾する必要があります。つまり、完全なパッケージ名を含めます。のようなものcom.domain.dto.SurveyAnswerReport
マニッシュ2016年

2
私からカスタム型を返そうとすると、「java.lang.IllegalArgumentException:PersistentEntity is not null!」が表示されますJpaRepository。一部の構成を見逃していますか?
marioosh 2017年

1
ネイティブクエリの例外を使用しているときに、ネストされた例外はjava.lang.IllegalArgumentException:マネージタイプではありません:クラス...なぜこれを処理する必要があるのですか?
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 2017

@manishありがとう、私の夜の眠りを救ってくれた、あなたの方法は魅力のように働いた!!!!!!!
-Vineel

15

これは古い質問であり、既に回答済みですが、別のアプローチを次に示します。

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

新しいクラスやインターフェースを作成する必要がないので、私はあなたの答えが好きです。それは私のために働いた。
Yuri

正常に動作しますが、Mapではキー(0)および値(1)としてアクセスできるため、ジェネリックで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();

}

残念ながら、GUIの観点からは、プロジェクションをDTOオブジェクトとして使用することはできません。フォームの送信にDTOを再利用したい場合はできません。それでも、ゲッター/セッターを備えた別の通常のBeanが必要です。したがって、それは良い解決策ではありません。
遺伝子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 2018

1

私はこの問題を解決しました:

  • クラスベースのプロジェクションはquery 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();

}

上記のコードは私のために働いた。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.