永続化エンティティ
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)
1 | select(…) メソッドで Person を使用すると、表形式の結果が Person 結果オブジェクトにマップされます。 |
2 | all() 行をフェッチすると、結果を制限することなく 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 句が生成されます。 |
4 | 1 つの結果を選択すると、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
演算子を使用して条件を作成します。SELECT
、UPDATE
、DELETE
クエリではCriteria
を使用できます。
データの挿入
insert()
エントリポイントを使用してデータを挿入できます。
次の単純な型指定された挿入操作を検討してください。
Mono<Person> insert = template.insert(Person.class) (1)
.using(new Person("John", "Doe")); (2)
1 | into(…) メソッドで 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)
1 | Person オブジェクトを更新し、マッピングメタデータに基づいてマッピングを適用します。 |
2 | inTable(…) メソッドを呼び出して、別のテーブル名を設定します。 |
3 | WHERE 句に変換されるクエリを指定します。 |
4 | Update オブジェクトを適用します。この場合、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)
1 | Person オブジェクトを削除し、マッピングメタデータに基づいてマッピングを適用します。 |
2 | from(…) メソッドを呼び出して、別のテーブル名を設定します。 |
3 | WHERE 句に変換されるクエリを指定します。 |
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 にバンプします。 |
4 | version = 0 がまだ残っている以前にロードされた行を更新してみてください。現在の version は 1 であるため、操作は OptimisticLockingFailureException で失敗します。 |