Jakarta EE 11 の目玉機能 Jakarta Data 1.0 まとめ

はじめに

Jakarta EE 11 の目玉機能である Jakarta data 1.0 の先取りまとめです。開発は遅れていますが、RC1が 2024/04/19 に リリース予定となっています。

Spring Data のようにリポジトリインターフェースによりデータベース操作が可能になります。 ザックリとした特徴は以下のようになります。

  • Jakarta Persistence と Jakarta NoSQL で利用可能
  • Repository インターフェースでリポジトリを定義
  • リポジトリでは、3 種類の方法でクエリが可能
    • @Query アノテーションによる annotated query method
      • JPQL のサブセットである JDQL(Jakarta Data Query Language) でクエリを定義
    • @Find@By アノテーションによる parameter based automatic query method
    • メソッドの命名規則による query by method name
  • Jakarta Data の Static Metamodel によりタイプセーフにエンティティ属性にアクセス可能
  • PageRequest によるページング要求
  • Page や CursoredPage によるページ操作


以下のようにリポジトリを定義することで、データベース操作が可能になります。

@Repository
public interface BookRepository extends CrudRepository<Book, Long> {

    @Find
    Book bookByIsbn(String isbn);

    @Find
    List<Book> booksByYear(Year year, Sort order, Limit limit);

    @Find
    Page<Book> find(@By("year") Year publishedIn, PageRequest<Book> pageRequest);
    
}


リポジトリ

リポジトリは @Repository アノテーションを付与することで定義します。この注釈は、インターフェイスがリポジトリを表すことを示すマーカーとして機能します。

組み込みのリポジトリインターフェースとして以下が提供されています。

public interface DataRepository<T, K> { }
public interface BasicRepository<T, K> extends DataRepository<T, K> { ... }
public interface CrudRepository<T, K> extends BasicRepository<T, K> { ... }

DataRepository はマーカーインターフェースです。

BasicRepository は単一のエンティティに適用される一般的な操作(@Save, @Find, @Delete)が定義されています。

CrudRepository@Insert@Update を含むCRUD 操作が定義されています。

通常は以下の様に CrudRepository を実装して使うことになるでしょう(Long はIDの型)。

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
}

組み込みのインターフェースを使わず、以下のようにカスタム・リポジトリ・インターフェイスを定義することもできます。

@Repository
public interface Garage {

    @Insert
    Car park(Car car);

    @Delete
    void unpark(Car car);
}

Jakarta data では以下の4つのライフサイクルアノテーションが提供されています。

public @interface Delete {}
public @interface Insert {}
public @interface Save {}
public @interface Update {}

このアノテーションが付けられた抽象メソッドはライフサイクルメソッドと呼ばれ、永続データに変更を加えることができます。Jakarta Persistence などの Jakarta data プロバイダでは @Merge のようなライフサイクルアノテーションが提供される可能性がありますが、移植可能でないため Jakarta data 側では提供されません。


Parameter based automatic query method

@Find アノテーションが付けられた抽象メソッドは、メソッドのパラメーターに基づいてクエリが推論されます。

public @interface Find {}

エンティティクラスの永続フィールドまたはプロパティと同じ名前の引数で条件を指定します。 異なる名前を指定する場合には、@By アノテーションにて以下のように指定できます。

@Find
Person findById(@By(ID) String id);

@Find
List<Person> findNamed(@By("firstName") String first,
                       @By("lastName") String last);

@Find
Person findByCity(@By("address.city") String city);

上記 address.city は、@By アノテーションを指定しない場合は、String address_city のような引数名を使います。

@By アノテーションは以下のような定義となっています。

public @interface By {
    String value();
    String ID = "id(this)";
}

Jakarta data 1.0 のリリース時には間に合わなそうですが、将来的には以下のようにアノテーション中に JDQL にて条件を指定できるよう検討が進んでいます。

@Find @Where("deleted = false")
@OrderBy("lower(title), isbn desc")
List<Book> longBooks(@By("pages > ?1") int minPages, 
                     @By("left(locale, 2) = lower(?2)") String language
                     @Pattern String topic)


Annotated query method

Query アノテーションでクエリを記述できます。

public @interface Query {
    String value();
}

クエリは、JPQL のサブセットである JDQL(Jakarta Data Query Language) を使うことができます(Jakarta NoSQLでも使えるように Jakarta Persistence に特化した仕様を省いたもの)。

名前付きパラメータで以下のようにパラメータを指定することができます。

@Query("where title like :title order by title")
Page<Book> booksByTitle(String title, PageRequest<Book> pageRequest);
@Query("where p.name = :prodname")
Optional<Product> findByName(@Param("prodname") String name);

位置パラメータを使った場合は以下のようになります。

@Query("delete from Book where isbn = ?1")
void deleteBook(String isbn);


Query by Method Name

個人的には好きではありませんが、Spring Data ではおなじみのメソッド名によるクエリが可能です。

List<Product> findByName(String name);

List<Product> findByNameLike(String namePattern);

@OrderBy(value = "price", descending = true)
List<Product> findByNameLikeAndPriceLessThan(String namePattern, float priceBelow);

メソッド名のキーワードには以下を使います。

キーワード 説明
findBy エンティティを返すクエリメソッド
deleteBy void か削除件数を返す削除クエリメソッド
countBy カウント件数を返す
existsBy 存在を boolean 結果で返す


Jakarta Data Static Metamodel

アプリケーションがタイプセーフな方法でエンティティ属性にアクセスできるようにする静的メタモデルが提供されます(アノテーションプロセッサにより自動生成するか、自身で定義することもできる)。

以下のエンティティがあった場合、

@Entity
public class Product {
  public long id;
  public String name;
  public float price;
}

以下のような Static Metamodel が生成されます。

@StaticMetamodel(Product.class)
public class _Product {
  public static final String ID = "id";
  public static final String NAME = "name";
  public static final String PRICE = "price";

  public static final SortableAttribute<Product> id = new SortableAttributeRecord<>("id");
  public static final TextAttribute<Product> name = new TextAttributeRecord<>("name");
  public static final SortableAttribute<Product> price = new SortableAttributeRecord<>("price");
}

Jakarta Persistence の静的メタモデルでは Product_ でしたが、Jakarta Data では先頭にアンダーステアが付いたものになります。

静的メタモデルにて、以下のようにタイプセーフなクエリ構築がサポートされます。

List<Product> found = products.findByNameLike(searchPattern,
                                              _Product.price.desc(),
                                              _Product.name.asc(),
                                              _Product.id.asc());


クエリ条件

クエリメソッドの引数には、Limit, Order, PageRequest、 または Sort を指定することができます。

それぞれ以下のような実装が提供されています。

public record Limit(int maxResults, long startAt) { ... }
public class Order<T> implements Iterable<Sort<? super T>> {
    private final List<Sort<? super T>> sorts;
    ...
}
public record Sort<T>(String property, boolean isAscending, boolean ignoreCase) { ... }
public interface PageRequest { ... }
record Pagination(
    long page, int size, Mode mode, Cursor type, boolean requestTotal)
    implements PageRequest {

以下のように、クエリの条件として使うことができます。

@Find
List<Product> productsByYear(Year year, Sort order, Limit limit);

@Find
List<Product> findByName(String name, PageRequest<Product> pageRequest);
PageRequest<Product> pageRequest =
        PageRequest.of(Product.class)
                   .size(20)
                   .page(1)
                   .sortBy(_Product.price.desc());
List<Product> first20 = products.findByName(name, pageRequest);

ソート条件は @OrderBy アノテーションにて以下のように定義することもできます。

@OrderBy("lastName")
@OrderBy("firstName")
@OrderBy("id")
Person[] findByZipCode(int zipCode, PageRequest pageRequest);


クエリ結果

クエリメソッドの戻り値には以下を指定することができます。

  • 配列型
  • List か Stream
  • Page か CursoredPage

CursoredPage は、結果の欠落や重複の表示を引き起こすことなく、ページの移動中にデータに対するある種の更新を許可できます。 CursoredPage では、数値位置に基づいてクエリを実行するのではなく、並べ替え基準としてエンティティ・プロパティキーを使用します。 並べ替え基準として使用されるエンティティ プロパティを変更したり、以前に削除したエンティティを追加する場合は、同じエンティティが再度表示されたり、値が変更されたために表示されなくなったりすることを防ぐことはできません。

CursoredPage を使ったページの移動は以下のように行うことができます。

@Repository
public interface CustomerRepository extends BasicRepository<Customer, Long> {
  @Query("WHERE totalSpent / totalPurchases > ?1")
  CursoredPage<Customer> withAveragePurchaseAbove(
      float minimum, PageRequest<Customer> pageRequest);
}
PageRequest<Customer> pageRequest = Order.by(_Customer.yearBorn.desc(),
                                             _Customer.name.asc(),
                                             _Customer.id.asc())
                                         .pageSize(25);
do {
  page = customers.withAveragePurchaseAbove(50.0f, pageRequest);
  ...
  if (page.hasNext())
    pageRequest = page.nextPageRequest();
} while (page.hasNext());


リソースへのアクセス

リポジトリにリソースアクセサーメソッドを定義することで、データ ストアへ直接アクセスすることができます。

例えば、Jakarta Data プロバイダ が JDBC に基づいている場合は、java.sql.Connectionまたは javax.sql.DataSource を取得でき、以下のように利用できます。

Connection connection();

default void cleanup() {
    try (Statement s = connection().createStatement()) {
        s.executeUpdate("truncate table books");
    }
}