Jakarta EE 11 で追加される Jakarta Data 1.0 概説


はじめに

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月時点の情報は以下を参照してください。

blog1.mammb.com


Jakarta Data 1.0 により提供されるもの

Jakarta Data 1.0 仕様には大きく以下が含まれます

  • @Repository アノテーションと BasicRepository PageableRepository インターフェース
    • @Query またはメソッド名またはパラメータによるクエリ定義
  • Slice PagePagination によるページング
    • 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 は、そのまま使ってサクッと開発を開始できる感じではないので、その点はとても良いとは思いますが、これを標準として定めることにすんなり腹落ちできないのはなぜだろう。