永続化エンティティ

R2dbcEntityTemplate は、Spring Data R2DBC の中心的なエントリポイントです。データのクエリ、挿入、更新、削除など、一般的なアドホックユースケース向けに、直接的なエンティティ指向の方法と、より狭く流れるようなインターフェースを提供します。

エントリポイント(insert()select()update() など)は、実行する操作に基づいた自然な命名スキーマに従います。エントリポイントから先に進むと、API は、SQL ステートメントを作成して実行する終了メソッドにつながるコンテキスト依存のメソッドのみを提供するように設計されています。Spring Data R2DBC は、R2dbcDialect 抽象化を使用して、バインドマーカー、ページ付けのサポート、基になるドライバーによってネイティブにサポートされるデータ型を決定します。

すべてのターミナルメソッドは、常に目的の操作を表す Publisher 型を返します。実際のステートメントは、サブスクリプション時にデータベースに送信されます。

エンティティを挿入および更新する方法

R2dbcEntityTemplate には、オブジェクトを保存および挿入するための便利な方法がいくつかあります。変換プロセスをよりきめ細かく制御するために、Spring コンバーターを R2dbcCustomConversions に登録できます(たとえば、Converter<Person, OutboundRow> や Converter<Row, Person>)。

保存操作を使用する単純なケースは、POJO を保存することです。この場合、テーブル名はクラスの名前(完全修飾ではない)によって決定されます。特定のコレクション名を使用して保存操作を呼び出すこともできます。マッピングメタデータを使用して、オブジェクトを格納するコレクションをオーバーライドできます。

挿入または保存するときに、Id プロパティが設定されていない場合、その値はデータベースによって自動生成されると想定されます。自動生成の場合、クラス内の Id プロパティまたはフィールドの型は、Long または Integer である必要があります。

次の例は、行を挿入してその内容を取得する方法を示しています。

R2dbcEntityTemplate を使用したエンティティの挿入と取得
Person person = new Person("John", "Doe");

Mono<Person> saved = template.insert(person);
Mono<Person> loaded = template.selectOne(query(where("firstname").is("John")),
		Person.class);

次の挿入および更新操作を使用できます。

同様の挿入操作のセットも利用できます。

  • Mono<T> insert (T objectToSave): オブジェクトをデフォルトのテーブルに挿入します。

  • Mono<T> update (T objectToSave): オブジェクトをデフォルトのテーブルに挿入します。

流れるような API を使用して、テーブル名をカスタマイズできます。

データの選択

R2dbcEntityTemplate の select(…) および selectOne(…) メソッドは、テーブルからデータを選択するために使用されます。どちらのメソッドも、フィールド射影、WHERE 句、ORDER BY 句、制限 / オフセットページングを定義する Query オブジェクトを取ります。制限 / オフセット機能は、基盤となるデータベースに関係なく、アプリケーションに対して透過的です。この機能は、個々の SQL フレーバー間の違いに対応するために R2dbcDialect の抽象化によってサポートされています。

R2dbcEntityTemplate を使用したエンティティの選択
Flux<Person> loaded = template.select(query(where("firstname").is("John")),
		Person.class);

Fluent API

このセクションでは、流れるような API の使用箇所について説明します。次の簡単なクエリについて考えてみます。

Flux<Person> people = template.select(Person.class) (1)
		.all(); (2)
1select(…) メソッドで Person を使用すると、表形式の結果が Person 結果オブジェクトにマップされます。
2all() 行をフェッチすると、結果を制限することなく Flux<Person> が返されます。

次の例では、名前、WHERE 条件、ORDER BY 句でテーブル名を指定するより複雑なクエリを宣言しています。

Mono<Person> first = template.select(Person.class)	(1)
	.from("other_person")
	.matching(query(where("firstname").is("John")			(2)
		.and("lastname").in("Doe", "White"))
	  .sort(by(desc("id"))))													(3)
	.one();																						(4)
1 テーブルから名前で選択すると、指定されたドメイン型を使用した行の結果が返されます。
2 発行されたクエリは、結果をフィルタリングするために firstname および lastname 列で WHERE 条件を宣言します。
3 結果は個々の列名で並べ替えることができ、結果として ORDER BY 句が生成されます。
41 つの結果を選択すると、1 つの行のみがフェッチされます。行を消費するこの方法では、クエリが正確に単一の結果を返すことが期待されます。クエリの結果が複数の場合、Mono は IncorrectResultSizeDataAccessException を発行します。
select(Class<?>) を介してターゲット型を指定することにより、射影を結果に直接適用できます。

次の終了方法を使用して、単一のエンティティの取得と複数のエンティティの取得を切り替えることができます。

  • first(): 最初の行のみを使用して、Mono を返します。クエリが結果を返さない場合、返された Mono はオブジェクトを発行せずに完了します。

  • one()Mono を返す 1 行のみを使用します。クエリが結果を返さない場合、返された Mono はオブジェクトを発行せずに完了します。クエリが複数の行を返す場合、Mono は例外的に IncorrectResultSizeDataAccessException を出力します。

  • all()Flux を返すすべての返された行を使用します。

  • count()Mono<Long> を返すカウント射影を適用します。

  • exists()Mono<Boolean> を返すことにより、クエリが行を生成するかどうかを返します。

select() エントリポイントを使用して、SELECT クエリを表現できます。結果の SELECT クエリは、よく使用される句(WHERE および ORDER BY)をサポートし、ページネーションをサポートします。流れるような API スタイルにより、チェーンは複数のメソッドを一緒に理解しながら、コードを理解しやすくなります。読みやすくするために、Criteria インスタンスの作成に「新しい」キーワードを使用しないようにする静的インポートを使用できます。

Criteria クラスのメソッド

Criteria クラスは次のメソッドを提供します。これらのメソッドはすべて SQL 演算子に対応しています。

  • Criteria and (String column): 指定された property を持つ連鎖 Criteria を現在の Criteria に追加し、新しく作成された Criteria を返します。

  • Criteria or (String column): 指定された property を持つ連鎖 Criteria を現在の Criteria に追加し、新しく作成された Criteria を返します。

  • Criteria greaterThan (Object o)> 演算子を使用して基準を作成します。

  • Criteria greaterThanOrEquals (Object o)>= 演算子を使用して基準を作成します。

  • Criteria in (Object…​ o): varargs 引数に IN 演算子を使用して、基準を作成します。

  • Criteria in (Collection<?> collection): コレクションを使用して IN 演算子を使用して、基準を作成します。

  • Criteria is (Object o): 列マッチング(property = value)を使用して基準を作成します。

  • Criteria isNull ()IS NULL 演算子を使用して基準を作成します。

  • Criteria isNotNull ()IS NOT NULL 演算子を使用して基準を作成します。

  • Criteria lessThan (Object o)< 演算子を使用して基準を作成します。

  • Criteria lessThanOrEquals (Object o) 演算子を使用して基準を作成します。

  • Criteria like (Object o): エスケープ文字処理なしで LIKE 演算子を使用して基準を作成します。

  • Criteria not (Object o)!= 演算子を使用して基準を作成します。

  • Criteria notIn (Object…​ o): varargs 引数に NOT IN 演算子を使用して、基準を作成します。

  • Criteria notIn (Collection<?> collection): コレクションを使用して NOT IN 演算子を使用して条件を作成します。SELECTUPDATEDELETE クエリでは Criteria を使用できます。

データの挿入

insert() エントリポイントを使用してデータを挿入できます。

次の単純な型指定された挿入操作を検討してください。

Mono<Person> insert = template.insert(Person.class)	(1)
		.using(new Person("John", "Doe")); (2)
1into(…) メソッドで Person を使用すると、マッピングメタデータに基づいて INTO テーブルが設定されます。また、挿入する Person オブジェクトを受け入れる insert ステートメントを準備します。
2 スカラー Person オブジェクトを提供します。または、Publisher を指定して、INSERT ステートメントのストリームを実行することもできます。このメソッドは、すべての非 null 値を抽出して挿入します。

データを更新する

update() エントリポイントを使用して行を更新できます。データの更新は、割り当てを指定する Update を受け入れて、更新するテーブルを指定することから始まります。また、Query を受け入れて WHERE 句を作成します。

次の単純な型付き更新操作を検討してください。

Person modified = …

		Mono<Long> update = template.update(Person.class)	(1)
				.inTable("other_table")														(2)
				.matching(query(where("firstname").is("John")))		(3)
				.apply(update("age", 42));												(4)
1Person オブジェクトを更新し、マッピングメタデータに基づいてマッピングを適用します。
2inTable(…) メソッドを呼び出して、別のテーブル名を設定します。
3WHERE 句に変換されるクエリを指定します。
4Update オブジェクトを適用します。この場合、age を 42 に設定し、影響を受ける行の数を返します。

データを削除する

delete() エントリポイントを使用して行を削除できます。データの削除は、削除するテーブルの指定から始まり、オプションで Criteria を受け入れて WHERE 句を作成します。

次の簡単な挿入操作を検討してください。

		Mono<Long> delete = template.delete(Person.class)	(1)
				.from("other_table")															(2)
				.matching(query(where("firstname").is("John")))		(3)
				.all();																						(4)
1Person オブジェクトを削除し、マッピングメタデータに基づいてマッピングを適用します。
2from(…) メソッドを呼び出して、別のテーブル名を設定します。
3WHERE 句に変換されるクエリを指定します。
4 削除操作を適用して、影響を受ける行の数を返します。

リポジトリを使用すると、ReactiveCrudRepository.save(…) メソッドでエンティティの保存を実行できます。エンティティが新しい場合、エンティティの挿入が行われます。

エンティティが新しくない場合は、更新されます。インスタンスが新しいかどうかは、インスタンスの状態の一部であることに注意してください。

このアプローチには、明らかな欠点がいくつかあります。参照されたエンティティのうち実際に変更されたものがわずかしかない場合、削除と挿入は無駄です。このプロセスは改善される可能性があり、おそらく改善される予定ですが、Spring Data R2DBC が提供できるものには特定の制限があります。集約の以前の状態はわかりません。そのため、更新プロセスは常にデータベースで見つかったものをすべて取得し、save メソッドに渡されたエンティティの状態に変換する必要があります。

ID 生成

Spring Data は、識別子プロパティを使用してエンティティを識別します。エンティティの ID には、Spring Data の @Id (Javadoc) アノテーションを付ける必要があります。

データベースに ID 列の自動インクリメント列がある場合、生成された値は、データベースに挿入された後にエンティティに設定されます。

Spring Data は、エンティティが新しく、識別子の値がデフォルトで初期値になっている場合、識別子の列の値を挿入しようとはしません。これは、プリミティブ型の場合は 0 であり、識別子プロパティが Long などの数値ラッパー型を使用している場合は null です。

エンティティ状態の検出では、エンティティが新しいかどうか、データベースに存在すると予想されるかどうかを検出する戦略について詳しく説明します。

重要な制約の 1 つは、エンティティを保存した後、そのエンティティが新しいものであってはならないことです。エンティティが新しいかどうかは、エンティティの状態の一部であることに注意してください。自動インクリメント列では、ID 列の値を使用して Spring Data によって ID が設定されるため、これは自動的に行われます。

楽観的ロック

Spring Data は、集約ルート上で @Version (Javadoc) のアノテーションが付けられた数値属性を使用してオプティミスティックロックをサポートします。Spring Data がそのようなバージョン属性を持つ集約を保存するときは常に、次の 2 つのことが起こります。

  • 集約ルートの更新ステートメントには、データベースに格納されているバージョンが実際に変更されていないことを確認する where 句が含まれます。

  • そうでない場合は、OptimisticLockingFailureException がスローされます。

また、エンティティとデータベースの両方でバージョン属性が増加するため、同時アクションは変更を認識し、上記で説明したように該当する場合は OptimisticLockingFailureException をスローします。

このプロセスは、新しい集合体の挿入にも適用されます。null または 0 バージョンは新しいインスタンスを示し、その後、増加したインスタンスはインスタンスを新規ではないものとしてマークします。UUID が使用されます。

削除中にバージョンチェックも適用されますが、バージョンは増加しません。

@Table
class Person {

  @Id Long id;
  String firstname;
  String lastname;
  @Version Long version;
}

R2dbcEntityTemplate template = …;

Mono<Person> daenerys = template.insert(new Person("Daenerys"));                      (1)

Person other = template.select(Person.class)
                 .matching(query(where("id").is(daenerys.getId())))
                 .first().block();                                                    (2)

daenerys.setLastname("Targaryen");
template.update(daenerys);                                                            (3)

template.update(other).subscribe(); // emits OptimisticLockingFailureException        (4)
1 最初に行を挿入します。version は 0 に設定されます。
2 挿入したばかりの行をロードします。version は 0 のままです。
3 行を version = 0 で更新します。lastname を設定し、version を 1 にバンプします。
4version = 0 がまだ残っている以前にロードされた行を更新してみてください。現在の version は 1 であるため、操作は OptimisticLockingFailureException で失敗します。