- はじめに
- リポジトリ
- Parameter based automatic query method
- Annotated query method
- Query by Method Name
- Jakarta Data Static Metamodel
- クエリ条件
- クエリ結果
- リソースへのアクセス
はじめに
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"); } }