はじめに
2024年3月末にリリース予定の Jakarta EE 11 では、以下の機能強化が予定されています。
- Java Record クラスのサポート
- Virtual Thread のサポート(これに伴い Java 21 が最低バージョンになります)
- Jakarta Data 1.0
この中では特に、Jakarta Data 1.0 が開発者視点ではインパクトが大きいのではないでしょうか。
Jakarta Data 1.0 では、Spring Data のような @Repository
が使えるようになり、リポジトリ・インターフェースのメソッド名でクエリを定義できるようになります。
メソッド名でクエリを定義するのは、個人的には悪手と思っているのであまり歓迎できないのですが、ページネーション回りのモデルが提供されるのは嬉しいところです。これで毎回自作していた Page
やら Slice
やらを使わずに済むようになります。
現在はベータ段階の仕様にはなりますが、ここで内容について見ておきましょう(2023年7月時点のbeta-3 からAPIは既に大きく変更されています)。
2024年04月時点の情報は以下を参照してください。
Jakarta Data 1.0 により提供されるもの
Jakarta Data 1.0 仕様には大きく以下が含まれます
@Repository
アノテーションとBasicRepository
PageableRepository
インターフェース@Query
またはメソッド名またはパラメータによるクエリ定義
Slice
Page
とPagination
によるページングKeysetAwareSlice
KeysetAwarePage
によるキーセットのページネーション
@StaticMetamodel
による Entity Attribute の生成
それぞれ順に見ていきましょう。
Repository
以下の Entity があった場合、
@Entity public class Product { @Id public long id; public String name; public float price; public int yearProduced; ... }
@Repository
アノテーションでリポジトリを以下のように定義できます。
@Repository public interface Products extends BasicRepository<Product, Long> { @OrderBy("price") List<Product> findByNameIgnoreCaseLikeAndPriceLessThan(String namePattern, float max); ... }
リポジトリは @Inject
でサービスクラスなどにインジェクトできます。
エンティティは、Jakarta Persistence の場合は jakarta.persistence.Entity
となり、Jakarta NoSQL の場合は Jakarta.nosql.mapping.Entity
となり、Jakarta Data ではこれらを透過的に扱います。
リポジトリは組み込みで以下のものが準備されています。
public interface DataRepository<T, K> {}
public interface BasicRepository<T, K> extends DataRepository<T, K> { // ... }
public interface PageableRepository<T, K> extends BasicRepository<T, K> { // ... }
BasicRepository
には基本的なメソッドが定義されているので、これを実装したリポジトリを定義することが多いでしょう。
なお、BasicRepository
の子として CrudRepository
を追加するプルリクエストもありますが、現時点ではマージされるかどうかは未定です。
クエリ定義
Jakarta Data では、以下の 3 つの方法でカスタム クエリを作成できます。
@Query
アノテーションによるクエリ定義- メソッド名によるクエリ定義
- パラメータによるクエリ定義(メソッドのパラメータ名とメソッド名のプレフィックスに基づいてクエリを定義)
@Query アノテーション
@Query
アノテーションにより、クエリをメソッドと紐づけて定義できます。
@Repository public interface ProductRepository extends BasicRepository<Product, Long> { @Query("SELECT p FROM Products p WHERE p.name=?1") // example in JPQL Optional<Product> findByName(String name); }
クエリ構文は、SQL、JPQL、Cypher、CQL などのベンダーやデータ ソースによって異なります。
@Param
バインダー アノテーションで以下のように定義したりすることもできます。
@Query("SELECT p FROM Products p WHERE p.name=:name") Optional<Product> findByName(@Param("name") String name);
@Query
アノテーションでクエリを定義することもできます。JPA の場合は JPQL を value 属性に定義します(カウント用のクエリも同時に定義できます)。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Query { String value(); String count() default ""; }
メソッド名によるクエリ定義
Spring Data と同様に、メソッド名によりクエリを定義することができます。
@Repository public interface ProductRepository extends BasicRepository<Product, Long> { List<Product> findByName(String name); @OrderBy("price") List<Product> findByNameLike(String namePattern); @OrderBy(value = "price", descending = true) List<Product> findByNameLikeAndPriceLessThan(String namePattern, float priceBelow); }
クエリメソッドのBNFは以下のように定義されます。
<query-method> ::= <subject> <predicate> [<order-clause>] <subject> ::= (<action> | "find" <find-expression>) "By" <action> ::= "find" | "delete" | "update" | "count" | "exists" <find-expression> ::= "First" [<positive-integer>] <predicate> ::= <condition> { ("And" | "Or") <condition> } <condition> ::= <property> ["IgnoreCase"] ["Not"] [<operator>] <operator> ::= "Contains" | "EndsWith" | "StartsWith" | "LessThan"| "LessThanEqual" | "GreaterThan" | "GreaterThanEqual" | "Between" | "Empty" | "Like" | "In" | "Null" | "True" | "False" <property> ::= <identifier> | <identifier> "_" <property> <identifier> ::= <word> <positive-integer> ::= <digit> { <digit> } <order-clause> ::= "OrderBy" { <order-item> } ( <order-item> | <property> ) <order-item> ::= <property> ("Asc" | "Desc")
子 Entity の条件を指定する場合は、そのまま連結するか _
で連結します。Address
エンティティ zipCode
フィールドの場合は findByAddressZipCode
とするか findByAddress_ZipCode
のようになります。
パラメータによるクエリ定義
Query by Parameters パターンによりクエリを定義できます。 すなわち、メソッドのパラメーターの名前からクエリ条件を決定します。
メソッド名のプレフィックスとして、メソッド名によるクエリ定義では findByXX
のように定義しましたが、パラメータによるクエリ定義では By
を除き find
でメソッド名を始めます。
@Repository public interface ProductRepository extends BasicRepository<Product, Long> { // yearProduced が一致する Product List<Product> findMadeIn(int yearProduced, Sort... sorts); // name と status が一致するものが存在するか? boolean existsWithStatus(String name, Status status); // yearProduced が一致する Product を削除 void deleteOutdated(int yearProduced); }
すべての条件は AND で結合されます。埋め込み属性の場合、_
をメソッドパラメータ名の区切り文字として使用できます。
ページング
リポジトリ Pageable
を条件に加え、 Page
または Slice
として結果の部分レコードを取得できます。
@OrderBy("age") Page<User> findByNameStartsWith(String namePrefix, Pageable pagination);
以下のように処理できます。
for (Pageable p = Pageable.ofSize(100).sortBy(Sort.asc("id")); p != null; p = page.length == 0 ? null : p.next()) { page = users.findByNameStartsWith("a", p); ... }
キーセット・ページネーション
キーセット・ページネーションは、ページング処理で問題となる、レコードの重複取得やレコードの取りこぼしを防ぐ目的で利用します。 通常のページング処理の場合、あるページの取得は、直前に取得したページとの相対的位置で決定しますが、キーセット・ページネーションでは、ページングをレコードを一意に特定するキーを元に行うため、重複や取りこぼしを防止できます。
KeysetAwareSlice
または KeysetAwarePage
を戻り値として指定することで、クエリに追加の条件を追加し、キーセットの値を自動的に追跡するようになります。
@Repository public interface CustomerRepository extends BasicRepository<Customer, Long> { KeysetAwareSlice<Customer> findByZipcodeOrderByLastNameAscFirstNameAscIdAsc( int zipcode, Pageable pageable); }
キーセット・ページネーションではレコードを一意に識別する必要があります。そのため、この条件を以下で指定する必要があります。
- リポジトリメソッドの
OrderBy
アノテーション Sort
またはPageable
で指定したソート条件
@StaticMetamodel
JPA のスタティック・メタモデルと被りますが、Jakarta Data でもエンティティ属性にタイプセーフでアクセスするための スタティック・メタモデル があります。Jakarta Persistence と Jakarta NoSQL、その他のデータアクセスレイヤを統一して扱いたいので、別の定義が必要になる のはわかりますが、OrderBy
など、色々重複していて気持ち悪いところはあります。
開発者は、@StaticMetamodel
で Entity に対応するメタモデルを定義することができます。
@StaticMetamodel(Product.class) public class Product_ { public static final Attribute id = Attribute.get(); public static final Attribute name = Attribute.get(); public static final Attribute price = Attribute.get(); }
Entity の属性名から、エンティティ属性が、Attribute
として初期化され、以下のようにタイプセーフにソート条件を定義することができます。
products.findByNameContains(searchPattern, Product_.price.desc(), Product_.name.asc(), Product_.id.asc());
そもそも、メソッド名でクエリ定義する時点でタイプセーフでは無いので、これだけだと嬉しいことはあまりありませんが。
まとめ
Jakarta EE 11 で導入予定となっている Jakarta Data 1.0 仕様の内容について概要を見ました。
現在 Jakarta Data 1.0 仕様は開発中の段階であり、変更される可能性はありますが、仕様としては概ね固まってきているように思われます。
Jakarta Persistence は、そのまま使ってサクッと開発を開始できる感じではないので、その点はとても良いとは思いますが、これを標準として定めることにすんなり腹落ちできないのはなぜだろう。