MENU

【Spring Boot】Spring Data JPAで集計処理を行い、その結果を別エンティティで受け取る

こんにちは、くまごろーです。
今家計簿アプリを作成してるんですが、「今までの出金記録を月毎に分け、集計する」というメソッドを実装したかったんですよね。で、SQLSUM関数を使用して、以下の様な結果を表示させることまでは簡単にできました。

ym sum
202009 333
202011 44444
202011 2322
202101 44444

 
でも、これをどうJava側に渡せばいいのかわからなくて、かなり長いこと苦戦しました。
まだ私自身理解しきれていないところもありますが、とりあえずの解決策として、今回やったことをまとめようと思います。
 
 
1 集計結果を受け取るJavaBeansを作成する
 
集計結果を納めるためのJavaBeansを新たに作成します。
このクラスは、@Entityアノテーションを付けてないので、DB上にテーブルが作成されることはありません。
完全に結果受け取り専用です。
今回は、SUM関数で、「年月」カラムと「合計金額」カラムを持った集計結果を表示するSQL文を考えているので、
以下の様な構成でJavaBeansを作成しました。

<MonthlySummary.java

@Data
public class MonthlySummary implements Serializable{

  private static final long serialVersionUID = 1L;

  private String month;
  private BigDecimal sum;

	
  public MonthlySummary(String month, BigDecimal sum) {  //コンストラクタ
    super();
    this.month = month;
    this.sum = sum;
  }
	
  public MonthlySummary(Object[] objects) {    //このメソッドについては3で後述
    this((String) objects[0], (BigDecimal) objects[1]);
  }
}

 
 
 
2 Repositoryクラスに、メソッドを用意する。
 
ネイティブクエリで書きました。というか、JPQLでの書き方がわかりませんでした・・・( ノД`)
とりあえず、純粋なSQL文を用意しました。
SQL文>

select  DATE_FORMAT(record_date, "%Y%m") as ym,
sum(income_and_expense) as sum
from money_records 
where user_id = 'kumaGoro95' and category_id not like '99%'  
//'99%'・・・収入カテゴリを除外 
group by DATE_FORMAT(record_date, "%Y%m") 
order by record_date asc;

これを最初、JPQLで記述しようとしたんですが、DATE_FORMATはJPQLで使用できないらしく・・・JPQLでDATE_FORMATと同様の働きをさせるにはどうすればよいのかわからず、ネイティブクエリで攻めることにしました。
 
※ JPQLでの記述は、超簡素ですがこちらの記事で少しまとめました。
【Spring Boot】Spring JPAともっと仲良くしたい【JPQL】 - くまごろーのプログラミングメモ

 
上記のsql文をネイティブクエリでJPAに落とし込むと、こんな感じになります。

<MoneyRecordRepository.java

public interface MoneyRecordRepository extends JpaRepository<MoneyRecord, Long>{
  @Query(value = "SELECT DATE_FORMAT(record_date, '%Y%m'), sum(income_and_expense) "
      +"FROM money_records where user_id = :username " 
      + " and category_id not like '99%' "  //'収入'カテゴリを除外
      + "group by DATE_FORMAT(record_date, '%Y%m') "
      + "order by record_date asc", nativeQuery = true)  //*1
  public List<Object[]> getMonthSummaries(@Param("username") String username);  //*2
	
  default List<MonthlySummary> findMonthSummaries(String username) {
    return getMonthSummaries(username).stream()  //*3
      .map(MonthlySummary::new)
      .collect(Collectors.toList());
  }
}

 

※1 ネイティブクエリで書く際は、以下の記述を忘れずに。

@Query(value = "sql文", nativeQuery = true)


※2 メソッドの戻り値型について
MoneyRecordRepository.javaは戻り値としてMonthlySummaryクラスを扱うことはできないが、Object[ ]は扱うことができる。
ひとまず、結果をObject[ ] で受け取り、その後の処理をdefaultメソッドに記載。

※3 Streamクラスについて
Streamクラスのmapメソッドで、Object[]をMonthlySummaryインスタンスに流し込んでいます。
Stream、正直全然わかってません。(参考書で一度触れた記憶はあるが・・・)
調べてみたけど、大体こんな感じなのかな?

(1)Streamメソッドで、Object[ ]をStreamに変換

return getMonthSummaries(username).stream()  //*3

(2)Object[ ]をMonthlySummaryに流し込む

 .map(MonthlySummary::new)

(3)collectメソッドで、StreamをListに変換

 .collect(Collectors.toList());

いまいちわかってないまま使ってしまってだめだなあと痛感してますが、今回触ってみて、これ意外と便利では?と思いました。for文とかと同じ役割ができるのでは?しかも1行で済むし・・・
 
 
 
3 JavaBeansに、Object[]を受け取るコンストラクタを追加
 
最後に、JavaBeansがObject型配列の引数でnewできるように、専用のコンストラクタを追加します。

  public MonthlySummary(Object[] objects) { 
    this((String) objects[0], (BigDecimal) objects[1]);
  }




3 参考
Spring Data JPAで複数テーブルを結合した結果を返すクエリを作る | memorandum
【Java】 Stream(filter, map, forEach, reduce) - Qiita
Stream API mapメソッド - Qiita