この記事の目的
SpringDataJPAを用いると、ページング処理が簡単に実装できるので便利です。
しかし、親子エンティティー(親1対子Nの構造、ヘッダーディティールと言ったりもする)を一緒にFetchする場合には、本来は親の件数を数えたいのに、子の件数に引っ張られてカウントが正しくできないことがありえます。
この記事では、そのような場合のカスタマイズ方法について紹介します
状況の説明
エンティティーの構造
請求エンティティー1件に対して、複数の支払が紐づく場合があり得るとします。(複数の支払い手段の併用などのケース)
問題となるケース
やりたいこととしては、期日がすぎても支払が未完了(未収状態/未払状態)の請求を抽出したいとします。
当然複数の請求が返却されるため、ページングの処理が必要になります。
ページ数を計算する際に必要なのは、1ページの件数と、結果セットの総件数です。
SpringDataJPAを使うとページング処理はよしなにやってくれるので便利ですが、総件数が子供エンティティ(この場合「支払」)に引っ張られて正しい件数が算出されない場合があります。
実際のクエリー(件数カウント)
標準の実装では、以下のようなクエリーが生成されます。
select
count(b1_0.billing_id)
from
billing b1_0
left join
payment p1_0
on b1_0.billing_id=p1_0.billing_id
where
b1_0.payment_due_date<=?
and (
p1_0.payment_id is null
or (
select
sum(p2_0.payment_amount)
from
payment p2_0
where
p2_0.billing_id=b1_0.billing_id
)<b1_0.billing_amount
);
期待されるクエリー
select
count(distinct b1_0.billing_id)
from
billing b1_0
left join
payment p1_0
on b1_0.billing_id=p1_0.billing_id
where
b1_0.payment_due_date<=?
and (
p1_0.payment_id is null
or (
select
sum(p2_0.payment_amount)
from
payment p2_0
where
p2_0.billing_id=b1_0.billing_id
)<b1_0.billing_amount
);
対応方法
いくつかの対応方法がありますが、今回はカスタムのRepositoryを実装する方法により対応しました。
CustomRepositoryのInterface定義
package org.example.domain.repository;
import java.time.LocalDate;
import org.immutable.domain.entity.Billing;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface CustomBillingRepository {
Page<Billing> findUnpaidBillings(LocalDate currentDate, Pageable pageable);
}
上記インタフェースの実装(ここにクエリーを書きます)
package org.example.domain.repository;
import java.time.LocalDate;
import java.util.List;
import org.immutable.domain.entity.Billing;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
public class CustomBillingRepositoryImpl implements CustomBillingRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Page<Billing> findUnpaidBillings(LocalDate currentDate, Pageable pageable) {
// メインクエリー
TypedQuery<Billing> query = entityManager.createQuery(
"SELECT b FROM Billing b " +
"LEFT JOIN Payment p ON b.id = p.billing.id " +
"WHERE b.paymentDueDate <= :currentDate " +
"AND (p IS NULL OR (SELECT SUM(p.paymentAmount) FROM Payment p WHERE p.billing.id = b.id) < b.billingAmount)", Billing.class);
query.setParameter("currentDate", currentDate);
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
List<Billing> billings = query.getResultList();
// カウントクエリー
TypedQuery<Long> countQuery = entityManager.createQuery(
"SELECT COUNT(DISTINCT b) FROM Billing b " +
"LEFT JOIN Payment p ON b.id = p.billing.id " +
"WHERE b.paymentDueDate <= :currentDate " +
"AND (p IS NULL OR (SELECT SUM(p.paymentAmount) FROM Payment p WHERE p.billing.id = b.id) < b.billingAmount)", Long.class);
countQuery.setParameter("currentDate", currentDate);
Long total = countQuery.getSingleResult();
return new PageImpl<>(billings, pageable, total);
}
}
CustomRepositoryを使ったBillingRepository
上記のインタフェースをextendsするだけです。
package org.immutable.domain.repository;
import org.immutable.domain.entity.Billing;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BillingRepository extends JpaRepository<Billing, Long>, CustomBillingRepository {
}
サービスクラスなどからの利用例
/**
* 支払い期日が到来しているのに未払いの請求を取得する.
* @param currentDate 現在日付
* @param pageable ページング情報
* @return 未払いの請求のページ
*/
public Page<Billing> getUnpaidBillings(LocalDate currentDate, Pageable pageable) {
return billingRepository.findUnpaidBillings(currentDate, pageable);
}
備考
この処理の実装については、ChatGPT(GPT-4o)に質問しつつ、ドキュメントを随時参照する方法をとりました。
GPTのおかげでスムーズに問題解決ができました。