🍃

Spring JPAのSpecificationを用いた検索サンプル

2023/10/01に公開

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