Open23

「Spring Data JDBC - リファレンスドキュメント」を読む

mskmsk

概要

「Spring Data JDBC - リファレンスドキュメント」を読む。
日本語なのでハードルが低いが、日本語訳が怪しい箇所がある。例えば、「Domain-driven design principles. -> ドメイン主導の設計原則」がある。可能であれば、日本語訳にコミットしたいけど、機械翻訳なのかもしれない。

https://spring.pleiades.io/spring-data/jdbc/docs/current/reference/html/

mskmsk

序文

Spring Data JDBC についてのイントロ。
DDD について軽く触れられている。

mskmsk

1. Spring の学習

Spring Data ではなく、Spring についての説明。
他のリファレンスのリンクが貼ってある。
理解は必須だけど、今回はスキップ。

mskmsk

2. 要件

バージョンについて言及。
使える DB は以下。JDBC で対応している RDB は揃っている。NoSQL(MongoDB、Redis、etc...)は専用の Spring Data(Spring Data Redis、Spring Data MongoDB)がある。

  • DB2
  • H2
  • HSQLDB
  • MariaDB
  • Microsoft SQL Server
  • MySQL
  • Oracle
  • Postgres
mskmsk

8. Spring Data リポジトリの操作

Spring Data の基本的な使い方。

https://spring.pleiades.io/spring-data/jdbc/docs/current/reference/html/#repositories

mskmsk

8.1. コアコンセプト

Spring Data は根本に Repository が存在する。
Repository をもとに操作する。

@Indexed
public interface Repository<T, ID> {

}

CrudRepository は Repository をもとに基本的な操作を具体化したもの。

CrudRepository
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

	/**
	 * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
	 * entity instance completely.
	 *
	 * @param entity must not be {@literal null}.
	 * @return the saved entity; will never be {@literal null}.
	 * @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}.
	 * @throws OptimisticLockingFailureException when the entity uses optimistic locking and has a version attribute with
	 *           a different value from that found in the persistence store. Also thrown if the entity is assumed to be
	 *           present but does not exist in the database.
	 */
	<S extends T> S save(S entity);

	/**
	 * Saves all given entities.
	 *
	 * @param entities must not be {@literal null} nor must it contain {@literal null}.
	 * @return the saved entities; will never be {@literal null}. The returned {@literal Iterable} will have the same size
	 *         as the {@literal Iterable} passed as an argument.
	 * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is
	 *           {@literal null}.
	 * @throws OptimisticLockingFailureException when at least one entity uses optimistic locking and has a version
	 *           attribute with a different value from that found in the persistence store. Also thrown if at least one
	 *           entity is assumed to be present but does not exist in the database.
	 */
	<S extends T> Iterable<S> saveAll(Iterable<S> entities);

	/**
	 * Retrieves an entity by its id.
	 *
	 * @param id must not be {@literal null}.
	 * @return the entity with the given id or {@literal Optional#empty()} if none found.
	 * @throws IllegalArgumentException if {@literal id} is {@literal null}.
	 */
	Optional<T> findById(ID id);

	/**
	 * Returns whether an entity with the given id exists.
	 *
	 * @param id must not be {@literal null}.
	 * @return {@literal true} if an entity with the given id exists, {@literal false} otherwise.
	 * @throws IllegalArgumentException if {@literal id} is {@literal null}.
	 */
	boolean existsById(ID id);

	/**
	 * Returns all instances of the type.
	 *
	 * @return all entities
	 */
	Iterable<T> findAll();

	/**
	 * Returns all instances of the type {@code T} with the given IDs.
	 * <p>
	 * If some or all ids are not found, no entities are returned for these IDs.
	 * <p>
	 * Note that the order of elements in the result is not guaranteed.
	 *
	 * @param ids must not be {@literal null} nor contain any {@literal null} values.
	 * @return guaranteed to be not {@literal null}. The size can be equal or less than the number of given
	 *         {@literal ids}.
	 * @throws IllegalArgumentException in case the given {@link Iterable ids} or one of its items is {@literal null}.
	 */
	Iterable<T> findAllById(Iterable<ID> ids);

	/**
	 * Returns the number of entities available.
	 *
	 * @return the number of entities.
	 */
	long count();

	/**
	 * Deletes the entity with the given id.
	 * <p>
	 * If the entity is not found in the persistence store it is silently ignored.
	 *
	 * @param id must not be {@literal null}.
	 * @throws IllegalArgumentException in case the given {@literal id} is {@literal null}
	 */
	void deleteById(ID id);

	/**
	 * Deletes a given entity.
	 *
	 * @param entity must not be {@literal null}.
	 * @throws IllegalArgumentException in case the given entity is {@literal null}.
	 * @throws OptimisticLockingFailureException when the entity uses optimistic locking and has a version attribute with
	 *           a different value from that found in the persistence store. Also thrown if the entity is assumed to be
	 *           present but does not exist in the database.
	 */
	void delete(T entity);

	/**
	 * Deletes all instances of the type {@code T} with the given IDs.
	 * <p>
	 * Entities that aren't found in the persistence store are silently ignored.
	 *
	 * @param ids must not be {@literal null}. Must not contain {@literal null} elements.
	 * @throws IllegalArgumentException in case the given {@literal ids} or one of its elements is {@literal null}.
	 * @since 2.5
	 */
	void deleteAllById(Iterable<? extends ID> ids);

	/**
	 * Deletes the given entities.
	 *
	 * @param entities must not be {@literal null}. Must not contain {@literal null} elements.
	 * @throws IllegalArgumentException in case the given {@literal entities} or one of its entities is {@literal null}.
	 * @throws OptimisticLockingFailureException when at least one entity uses optimistic locking and has a version
	 *           attribute with a different value from that found in the persistence store. Also thrown if at least one
	 *           entity is assumed to be present but does not exist in the database.
	 */
	void deleteAll(Iterable<? extends T> entities);

	/**
	 * Deletes all entities managed by the repository.
	 */
	void deleteAll();
}

PagingAndSortingRepository インターフェースを用いると、ページネーションも可能。

PagingAndSortingRepository

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

	/**
	 * Returns all entities sorted by the given options.
	 *
	 * @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be
	 *          {@literal null}.
	 * @return all entities sorted by the given options
	 */
	Iterable<T> findAll(Sort sort);

	/**
	 * Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
	 *
	 * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
	 *          {@literal null}.
	 * @return a page of entities
	 */
	Page<T> findAll(Pageable pageable);
}

mskmsk

8.2 クエリメソッド

CRUDRepository も定義されているけど、エンティティのプロパティからメソッド名を推論してくれる。

interface PersonRepository extends Repository<Person, Long> {}

interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}

mskmsk

8.3. リポジトリインターフェースの定義

特定のものだけ公開したい場合は、Repositoryを継承する。CRUD メソッドが確定で公開する場合は、CRUDRepository を使う

8.3.1. リポジトリ定義の微調整

Repository で特定のメソッドを公開する方法。以下は、CrudRepository にも公開されているが、Repository を継承したメソッドにあらためて定義することで、一部の機能のみに限定可能。

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {

  Optional<T> findById(ID id);

  <S extends T> S save(S entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

8.3.2. 複数の Spring Data モジュールでリポジトリを使用する

Spring Data は他の SpringData モジュールと共存可能。
Spring Data モジュール固有のアノテーション(@Entity、@Document)をつけて、Spring Data JDBC の Repository を利用できる。
しかし、Spring Data モジュール固有のアノテーションをモジュール間をまたいでつけることはできない。例えば、1 つのエンティティに @Entity、@Document を付与して、Repositoryを利用できない。

mskmsk

8.4. クエリメソッドの定義

クエリの生成お方法は 2 つ。

  • メソッド名から直接クエリを導出します。
  • 手動で定義されたクエリを使用します。

日本語訳的に言っている意味がわかりづらいけど、Repository の Generics に設定したエンティティのプロパティから推論してクエリを生成できる。
直感的ではあるけど、抽象度が高過ぎて生 SQL を書く人からすると驚く内容だと思う。

mskmsk

8.4.1. クエリ検索戦略

クエリを決定するときに、以下の手順らしい。
どれも日本語訳がわからなかった。

  • CREATE: クエリメソッド名から生成(?)
  • USE_DECLARED_QUERY: Query アノテーションから生成(?)
  • CREATE_IF_NOT_FOUND:CREATEとUSE_DECLARED_QUERYの組み合わせ(?)
mskmsk

8.4.2. クエリ作成

以下はドキュメントから引用。

Person エンティティには、emailAddress、lastname、firstname のプロパティがある模様。
主語と述語の書き方で分類できる。
主語は find...By のように書かれる。... にエンティティ、Top、First、Distinct なども入る。パターンと意味についてはドキュメント を参照。
述語で、プロパティを指定したり、クエリの WHERE 句の詳細な条件を指定している。
こちらも詳しい種類はドキュメントを参照。

interface PersonRepository extends Repository<Person, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
mskmsk

8.4.3. プロパティ式

プロパティのプロパティをさらに検索できるらしい。
例えば、Person エンティティの Address プロパティには、ZipCode プロパティがある。
その場合、直接以下のように参照できるらしい。つまりx.address.zipCode で探索する。

List<Person> findByAddressZipCode(ZipCode zipCode);

プロパティを分割するアルゴリズムがあって、順番は以下になる。

  • AddressZipCode で探索 -> 失敗
  • AddressZip と Code で探索 -> 失敗
  • Address と ZIpCode で探索 -> 成功

想定していないパターンで検索が引っかかる可能性がある。例えば、Person に addressZip プロパティがあったとき、code が存在しないため、失敗する。

mskmsk

8.4.4. 特別なパラメーター処理

プロパティ以外に、Pageable 、Sort といった型を用いて、ページネーションとソートをおこなう。

mskmsk

8.4.5. クエリ結果の制限

クエリの個数を Top と First を用いて制限できる。

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);
mskmsk

8.4.6. コレクションまたはイテラブルを返すリポジトリメソッド

Iterable、List、Set をサポートしている。
Spring Data には、Streamable 、Iterable のカスタム拡張があるらしい。
また、Java で関数型プログラミングするための vavr が存在し、それのサポートもしているらしい。

mskmsk

8.4.7. リポジトリメソッドの null 処理

Spring Data JDBC の null に対する対応。

以下をサポート

  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option

SpringFramework の nullablitiy アノテーションを利用できる。

  • @NonNullApi (Javadoc) : パッケージレベルで使用され、パラメーターと戻り値のデフォルトの動作が、それぞれ null 値を受け入れることも生成することもないことを宣言します。
  • @NonNull (Javadoc) : null であってはならないパラメーターまたは戻り値で使用されます(@NonNullApi が適用されるパラメーターおよび戻り値では不要です)。
  • @Nullable (Javadoc) : null の可能性があるパラメーターまたは戻り値で使用されます。

Kotlin では null 非許容型と許容型 が言語に組み込まれているので、そちらを使う。Spring Data も対応している。

mskmsk

8.4.8. クエリ結果のストリーミング

段階的にクエリ実行(for ループで SELECT)ができるらしい。
よくわからなかった。

mskmsk

8.4.9. 非同期クエリ結果

非同期でクエリメソッドを実行できる。
リアクティブクエリではなく、メソッドの呼び出し直後に、戻り値が返されるだけ。

以下は、ドキュメントの例。

@Async
Future<User> findByFirstname(String firstname);               

@Async
CompletableFuture<User> findOneByFirstname(String firstname); 

@Async
ListenableFuture<User> findOneByLastname(String lastname);