Specification

JPA の Criteria API を使用すると、プログラムでクエリを作成できます。Spring Data JPA Specification は、エンティティに対する述語を表現し、リポジトリ間で再利用するための、小規模で集中的な API を提供します。Eric Evans の著書「ドメイン駆動設計 [Amazon] 」の仕様コンセプトに基づいて、これらの仕様は同じセマンティクスに従い、JPA を使用して条件を定義するための API を提供します。仕様をサポートするには、次のように JpaSpecificationExecutor インターフェースを使用してリポジトリインターフェースを継承できます。

public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {
}

仕様とは、Criteria API で表現されたエンティティに対する述語です。Spring Data JPA は 2 つのエントリポイントを提供します。

  • PredicateSpecification : Spring Data JPA 4.0 で導入された、クエリ型に依存しない柔軟なインターフェース。

  • Specification (および UpdateSpecificationDeleteSpecification): クエリバインドバリアント。

PredicateSpecification

PredicateSpecification インターフェースは、幅広い機能構成を可能にする最小限の依存関係セットで定義されています。

public interface PredicateSpecification<T> {
  Predicate toPredicate(From<?, T> from, CriteriaBuilder builder);
}

仕様を使用すると、拡張可能な述語のセットを簡単に構築でき、次の例に示すように、必要な組み合わせごとにクエリ (メソッド) を宣言する必要がなくなり、JpaRepository と併用できます。

例 1: 顧客の仕様
class CustomerSpecs {

  static PredicateSpecification<Customer> isLongTermCustomer() {
    return (from, builder) -> {
      LocalDate date = LocalDate.now().minusYears(2);
      return builder.lessThan(from.get(Customer_.createdAt), date);
    };
  }

  static PredicateSpecification<Customer> hasSalesOfMoreThan(MonetaryAmount value) {
    return (from, builder) -> {
      // build predicate for sales > value
    };
  }
}

Customer_ 型は、JPA メタモデルジェネレーター(Hibernate 実装のドキュメントの例 (英語) を参照)を用いて生成されたメタモデル型です。式 Customer_.createdAt は、Customer が LocalDate 型の createdAt 属性を持つことを前提としています。さらに、ビジネス要件の抽象化レベルでいくつかの条件を規定し、実行可能な Specifications を作成しました。

リポジトリで仕様を直接使用します。

List<Customer> customers = customerRepository.findAll(isLongTermCustomer());

仕様は次のように構成されたときに最も価値が高まります。

MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
  isLongTermCustomer().or(hasSalesOfMoreThan(amount))
);

SpecificationUpdateSpecificationDeleteSpecification

Specification (Javadoc) インターフェースは以前から利用可能であり、Criteria API の制限に従って特定のクエリ型(選択、更新、削除)に関連付けられています。3 つの仕様インターフェースは以下のように定義されています。

  • 仕様

  • UpdateSpecification

  • DeleteSpecification

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder);
}
public interface UpdateSpecification<T> {
  Predicate toPredicate(Root<T> root, CriteriaUpdate<T> update,
            CriteriaBuilder builder);
}
public interface DeleteSpecification<T> {
  Predicate toPredicate(Root<T> root, CriteriaDelete<T> delete,
            CriteriaBuilder builder);
}

Specification オブジェクトは、次の例に示すように、直接構築することも、PredicateSpecification インスタンスを再利用して構築することもできます。

例 2: 顧客の仕様
public class CustomerSpecs {

  public static UpdateSpecification<Customer> updateLastnameByFirstnameAndLastname(String newLastName, String currentFirstname, String currentLastname) {
    return UpdateSpecification.<Customer>update((root, update, criteriaBuilder) -> {
      update.set("lastname", newLastName);
    }).where(hasFirstname(currentFirstname).and(hasLastname(currentLastname)));
  }

  public static PredicateSpecification<Customer> hasFirstname(String firstname) {
    return (root, builder) -> {
      return builder.equal(root.get("firstname"), firstname);
    };
  }

  public static PredicateSpecification<Customer> hasLastname(String lastname) {
    return (root, builder) -> {
      // build query here
    };
  }
}

Fluent API

JpaSpecificationExecutor は、Specification インスタンスに基づいてクエリを柔軟に実行するための流れるようなクエリメソッドを定義します。

  1. PredicateSpecification の場合: findBy(PredicateSpecification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction)

  2. Specification の場合: findBy(Specification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction)

他のメソッドと同様に、Specification から派生したクエリを実行します。ただし、クエリ関数を使用すると、他の方法では動的に制御できないクエリ実行の側面を制御できます。これは、SpecificationFluentQuery のさまざまな中間メソッドと終端メソッドを呼び出すことによって実現されます。

中間的な方法

  • sortBy: 結果に順序付けを適用します。メソッドを繰り返し呼び出すと、各 Sort が追加されます(ソート済みの Pageable を使用する page(Pageable) は、以前のソート順序を上書きすることに注意してください)。

  • limit: 結果数を制限します。

  • as: 読み取るまたは投影する型を指定します。

  • project: クエリのプロパティを制限します。

ターミナル方式

  • firstfirstValue: 最初の値を返します。クエリが結果を返さなかった場合、first は Optional<T> または Optional.empty() を返します。firstValue は、Optional を使用する必要がない、null 値可能なバリアントです。

  • oneoneValue: 1 つの値を返します。クエリが結果を返さなかった場合、one は Optional<T> または Optional.empty() を返します。oneValue は、Optional を使用する必要がない、NULL 値許容型のバリアントです。複数の一致が見つかった場合は、IncorrectResultSizeDataAccessException がスローされます。

  • all: すべての結果を List<T> として返します。

  • page(Pageable): すべての結果を Page<T> として返します。

  • slice(Pageable): すべての結果を Slice<T> として返します。

  • scroll(ScrollPosition): スクロール (オフセット、キーセット) を使用して、結果を Window<T> として取得します。

  • stream(): 結果を具体化された Collection ではなくストリームとして処理するには、Stream<T> を返します (Stream のセマンティクスを参照)。ストリームは状態を持つため、使用後は閉じる必要があります。

  • count および exists: 一致するエンティティの数、または一致するものが存在するかどうかを返します。

中間メソッドとターミナルメソッドは、クエリ関数内で呼び出す必要があります。
例 3: Fluent API を使用して、lastname で順序付けられた投影された Page を取得します。
Page<CustomerProjection> page = repository.findBy(spec,
    q -> q.as(CustomerProjection.class)
          .page(PageRequest.of(0, 20, Sort.by("lastname")))
);
例 4: 流れるような API を使用して、lastname 順に並べられた多数の結果の最後を取得します。
Optional<Customer> match = repository.findBy(spec,
    q -> q.sortBy(Sort.by("lastname").descending())
          .first()
);