Spring JPAのSpecificationを用いた検索サンプル
Specificationとは?
Spring JPAで検索処理をするときに、findByItemName()
のような単純な検索はできるけど、WHERE句が複雑になった場合に条件をうまく組めなかったので、その時の忘備録です。
JPA specificationとは、複雑な条件を持つクエリを簡単に実装するためのインタフェースになります。
実装サンプル
説明はサンプルコードを元に進めます。個人作成しているアプリケーションになります。
ただしサンプルコードの仕様を見なくとも、本記事で解決するように記述したいと思います。
エンティティの構成
集約モデルで構成したエンティティがあります。値オブジェクトとして色々子を持つツリー構造になっています。
今回書くSQLは、この値オブジェクトに対して、複数の条件を組み合わせた検索をしたいと思います。
また、以下の記述ではJPA modelgenを使用した記述でフィールド名を定義します。具体的にはEntity_.FIELD
のようなイメージです。
発行したいSQL
:todayは今日の日付が入る変数、:alert_definition_date、:modelはアプリケーション側から与えられる変数とします。
SELECT
*
FROM
stored_item
WHERE
expiration_date > (:today - :alert_definition_date)
AND alert_status_flag = "not_reported"
AND item_type = :model
Specificationクラスを作成する
modelは:alert_definition_dateを持つ変数としてメソッドに与えます。
また(:today - :alert_definition_date)
はLocalDateTime.now().minusDays(model.getExpirationDate())
に同じとして以下メソッドを実装します。
public Specification<StoredItem> warnedItem(ItemTypeModel model) {
return new Specification<StoredItem>() {
@Override
public Predicate toPredicate(Root<StoredItem> root, CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();
// WHERE expiration_date > :today - alert definition date
predicates.add(criteriaBuilder.greaterThan(root.get(StoredItem_.ITEM_DETAIL)
.get(ItemDetail_.EXPIRATION_DATE)
.get(ExpirationDate_.DATE),
LocalDateTime.now().minusDays(model.getExpirationDate())));
// AND alert_status_flag = NOT_REPORTED (not to include reported items)
predicates.add(criteriaBuilder.equal(root.get(StoredItem_.ALERT_STATUS_FLAG),
AlertStatusFlag.NOT_REPORTED));
// AND item_type = :model
predicates.add(criteriaBuilder.equal(root.get(StoredItem_.ITEM_DETAIL)
.get(ItemDetail_.ITEM_TYPE).get(ItemType_.MODEL), model));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
}
};
}
順に見ていきます。
まずSpecificationとして返却する値に、Specificationのインスタンスを作成し、toPredicate()
をオーバライドさせます。
ちなみにpredicateとは記述している個所、述部を意味する英語です。
ここで条件となるpredicatesをリスト形式にして作成します。
そのあと、creteriaBuilder.条件()を用意し、WHERE句を組んでいきます。
greaterThan(a, b)はa > bのようなイメージですね。
それぞれの第一引数、つまりFieldを定義するには、集約rootから順に子のオブジェクトを呼び出し、目的の値オブジェクトまでつなげていくイメージです。
上の例ではJpa modelgenを使用していますが、フィールド名をハードコーディングしても実装可能です。
最後にcriteriaBuilder.and(predicates.toArray(new Predicate[0]))
を返却することでほしいSQLを発行することができます。
呼び出し時にはJpaSpecificationのfindAll()の引数にこのSpecificationを与えれば、その条件でのクエリを実行することができます。
Discussion