R2DBC によるデータアクセス
R2DBC (英語) ("Reactive Relational Database Connectivity")は、リアクティブパターンを使用して SQL データベースへのアクセスを標準化するためのコミュニティ主導の仕様です。
パッケージ階層
Spring Framework の R2DBC 抽象化フレームワークは、2 つの異なるパッケージで構成されています。
core
:org.springframework.r2dbc.core
パッケージには、DatabaseClient
クラスとさまざまな関連クラスが含まれています。R2DBC コアクラスを使用した基本的な R2DBC 処理とエラー処理の制御を参照してください。connection
:org.springframework.r2dbc.connection
パッケージには、ConnectionFactory
に簡単にアクセスできるユーティリティクラスと、変更されていない R2DBC のテストと実行に使用できるさまざまなシンプルなConnectionFactory
実装が含まれています。データベース接続の制御を参照してください。
R2DBC コアクラスを使用した基本的な R2DBC 処理とエラー処理の制御
このセクションでは、R2DBC コアクラスを使用して、エラー処理を含む基本的な R2DBC 処理を制御する方法について説明します。次のトピックが含まれます。
DatabaseClient
を使用する
DatabaseClient
は、R2DBC コアパッケージの中心的なクラスです。リソースの作成と解放を処理します。これにより、接続を閉じるのを忘れるなどの一般的なエラーを回避できます。SQL を提供して結果を抽出するアプリケーションコードを残して、コア R2DBC ワークフローの基本的なタスク(ステートメントの作成や実行など)を実行します。DatabaseClient
クラス:
SQL クエリを実行します
ステートメントとストアドプロシージャの呼び出しを更新する
Result
インスタンスに対して反復を実行しますR2DBC 例外をキャッチし、
org.springframework.dao
パッケージで定義された一般的でより有益な例外階層に変換します。( 一貫した例外階層を参照してください。)
クライアントには、宣言型構成にリアクティブ型を使用する関数で流れるような API があります。
コードに DatabaseClient
を使用する場合は、java.util.function
インターフェースを実装するだけでよく、明確に定義された契約が提供されます。DatabaseClient
クラスによって提供される Connection
を指定すると、Function
コールバックは Publisher
を作成します。Row
の結果を抽出するマッピング関数についても同様です。
ConnectionFactory
参照を使用して直接インスタンス化することにより、DAO 実装内で DatabaseClient
を使用するか、Spring IoC コンテナーで構成して Bean 参照として DAO に渡すことができます。
DatabaseClient
オブジェクトを作成する最も簡単な方法は、次のように静的ファクトリメソッドを使用することです。
Java
Kotlin
DatabaseClient client = DatabaseClient.create(connectionFactory);
val client = DatabaseClient.create(connectionFactory)
ConnectionFactory は、常に Spring IoC コンテナーで Bean として構成する必要があります。 |
上記の方法は、デフォルト設定で DatabaseClient
を作成します。
DatabaseClient.builder()
から Builder
インスタンスを取得することもできます。次のメソッドを呼び出すことにより、クライアントをカスタマイズできます。
… .bindMarkers(…)
: 特定のBindMarkersFactory
を指定して、名前付きパラメーターからデータベースバインドマーカーへの変換を構成します。… .executeFunction(…)
:Statement
オブジェクトの実行方法をExecuteFunction
に設定します。… .namedParameters(false)
: 名前付きパラメーターの展開を無効にします。デフォルトで有効になっています。
ダイアレクトは、通常 ConnectionFactoryMetadata をインスペクションすることにより、ConnectionFactory から BindMarkersFactoryResolver (Javadoc) によって解決されます。org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider 〜 META-INF/spring.factories を実装するクラスを登録することにより、Spring に BindMarkersFactory を自動検出させることができます。BindMarkersFactoryResolver は、Spring の SpringFactoriesLoader を使用して、クラスパスからバインドマーカープロバイダーの実装を検出します。 |
現在サポートされているデータベースは次のとおりです。
H2
MariaDB
Microsoft SQL Server
MySQL
Postgres
このクラスによって発行されたすべての SQL は、クライアントインスタンスの完全修飾クラス名(通常 DefaultDatabaseClient
)に対応するカテゴリの DEBUG
レベルでログに記録されます。さらに、各実行は、デバッグを支援するために反応シーケンスにチェックポイントを登録します。
以下のセクションでは、DatabaseClient
の使用例をいくつか示します。これらの例は、DatabaseClient
によって公開されたすべての機能の完全なリストではありません。詳細については、付随する javadoc を参照してください。
ステートメントの実行
DatabaseClient
は、ステートメントを実行する基本機能を提供します。次の例は、新しいテーブルを作成する最小限で完全に機能するコードに含める必要があるものを示しています。
Java
Kotlin
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.then();
client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.await()
DatabaseClient
は、便利で流れるように使用できるように設計されています。実行仕様の各段階で中間、継続、終了メソッドを公開します。上記の例では、then()
を使用して、クエリ(または SQL クエリに複数のステートメントが含まれる場合はクエリ)が完了するとすぐに完了する完了 Publisher
を返します。
execute(…) は、SQL クエリ文字列またはクエリ Supplier<String> のいずれかを受け入れて、実際のクエリの作成を実行まで延期します。 |
クエリ (SELECT
)
SQL クエリは、Row
オブジェクトまたは影響を受ける行の数を通じて値を返すことができます。DatabaseClient
は、発行されたクエリに応じて、更新された行の数または行自体を返すことができます。
次のクエリは、テーブルから id
列と name
列を取得します。
Java
Kotlin
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
.fetch().first();
val first = client.sql("SELECT id, name FROM person")
.fetch().awaitSingle()
次のクエリはバインド変数を使用します。
Java
Kotlin
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().first();
val first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().awaitSingle()
上記の例で fetch()
の使用に気づいたかもしれません。fetch()
は、消費するデータ量を指定できる継続演算子です。
first()
を呼び出すと、結果から最初の行が返され、残りの行が破棄されます。次の演算子を使用してデータを使用できます。
first()
は、結果全体の最初の行を返します。その Kotlin コルーチンバリアントは、null 許容でない戻り値の場合はawaitSingle()
と呼ばれ、値がオプションの場合はawaitSingleOrNull()
と呼ばれます。one()
は正確に 1 つの結果を返し、結果にさらに行が含まれている場合は失敗します。Kotlin コルーチンを使用して、正確に 1 つの値にawaitOne()
を使用するか、値がnull
の場合はawaitOneOrNull()
を使用します。all()
は、結果のすべての行を返します。Kotlin コルーチンを使用する場合は、flow()
を使用してください。rowsUpdated()
は、影響を受ける行の数(INSERT
/UPDATE
/DELETE
カウント)を返します。その Kotlin コルーチンバリアントはawaitRowsUpdated()
という名前です。
さらにマッピングの詳細を指定せずに、クエリは表形式の結果を Map
として返します。そのキーは、列の値にマップされる大文字と小文字を区別しない列名です。
Row
ごとに呼び出される Function<Row, T>
を提供することで、結果のマッピングを制御できるため、任意の値 (特異値、コレクションとマップ、オブジェクト) を返すことができます。
次の例では、name
列を抽出し、その値を出力します。
Java
Kotlin
Flux<String> names = client.sql("SELECT name FROM person")
.map(row -> row.get("name", String.class))
.all();
val names = client.sql("SELECT name FROM person")
.map{ row: Row -> row.get("name", String.class) }
.flow()
あるいは、単一の値にマッピングするためのショートカットもあります。
Flux<String> names = client.sql("SELECT name FROM person")
.mapValue(String.class)
.all();
または、Bean プロパティまたはレコードコンポーネントを使用して結果オブジェクトにマップすることもできます。
// assuming a name property on Person
Flux<Person> persons = client.sql("SELECT name FROM person")
.mapProperties(Person.class)
.all();
DatabaseClient
で(INSERT
、UPDATE
、DELETE
)を更新
ステートメントの変更の唯一の違いは、これらのステートメントは通常、表形式のデータを返さないため、rowsUpdated()
を使用して結果を消費することです。
次の例は、更新された行の数を返す UPDATE
ステートメントを示しています。
Java
Kotlin
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().rowsUpdated();
val affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().awaitRowsUpdated()
クエリへの値のバインド
一般的なアプリケーションでは、何らかの入力に従って行を選択または更新するために、パラメーター化された SQL ステートメントが必要です。これらは通常、WHERE
句によって制約された SELECT
ステートメント、または入力パラメーターを受け入れる INSERT
および UPDATE
ステートメントです。パラメーター化されたステートメントは、パラメーターが適切にエスケープされない場合、SQL インジェクションのリスクを負います。DatabaseClient
は R2DBC の bind
API を利用して、クエリパラメーターの SQL インジェクションのリスクを排除します。execute(…)
演算子を使用してパラメーター化された SQL ステートメントを提供し、パラメーターを実際の Statement
にバインドできます。次に、R2DBC ドライバーは、準備されたステートメントとパラメーター置換を使用してステートメントを実行します。
パラメーターバインディングは、2 つのバインディング戦略をサポートしています。
インデックスによる、ゼロベースのパラメーターインデックスの使用。
名前別、プレースホルダー名を使用。
次の例は、クエリのパラメーターバインドを示しています。
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
あるいは、名前と値のマップを渡すこともできます。
Map<String, Object> params = new LinkedHashMap<>();
params.put("id", "joe");
params.put("name", "Joe");
params.put("age", 34);
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindValues(params);
または、Bean プロパティまたはレコードコンポーネントを含むパラメーターオブジェクトを渡すこともできます。
// assuming id, name, age properties on Person
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindProperties(new Person("joe", "Joe", 34);
あるいは、位置パラメーターを使用して、値をステートメントにバインドすることもできます。インデックスは 0 から始まります。
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind(0, "joe")
.bind(1, "Joe")
.bind(2, 34);
アプリケーションが多数のパラメーターにバインドされている場合は、1 回の呼び出しで同じことを実現できます。
List<?> values = List.of("joe", "Joe", 34);
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindValues(values);
クエリプリプロセッサーは、名前の Collection
パラメーターを一連のバインドマーカーに展開し、引数の数に基づいて動的なクエリを作成する必要をなくします。ネストされたオブジェクト配列は、(たとえば)選択リストを使用できるように拡張されています。
次のクエリを検討してください。
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
上記のクエリは、次のようにパラメーター化して実行できます。
Java
Kotlin
List<Object[]> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann", 50});
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
.bind("tuples", tuples);
val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
.bind("tuples", tuples)
選択リストの使用はベンダーに依存します。 |
次の例は、IN
述語を使用したより単純なバリアントを示しています。
Java
Kotlin
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", Arrays.asList(35, 50));
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", arrayOf(35, 50))
R2DBC 自体は、コレクションのような値をサポートしていません。ただし、上記の例で特定の List を展開すると、Spring の R2DBC サポートの名前付きパラメーター、たとえば上記の IN 句での使用に機能します。ただし、配列型の列 (たとえば Postgres) を挿入または更新するには、基盤となる R2DBC ドライバーでサポートされている配列型が必要です。通常は Java 配列で、たとえば text[] 列を更新するには String[] が必要です。Collection<String> などを配列パラメーターとして渡さないでください。 |
ステートメントフィルター
場合によっては、実際の Statement
を実行する前にオプションを微調整する必要があります。これを行うには、次の例に示すように、Statement
フィルター (StatementFilterFunction
) を DatabaseClient
に登録して、実行中のステートメントをインターセプトして変更します。
Java
Kotlin
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …);
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) }
.bind("name", …)
.bind("state", …)
DatabaseClient
は、Function<Statement, Statement>
を受け入れる単純化された filter(…)
オーバーロードも公開します。
Java
Kotlin
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"));
client.sql("SELECT id, name, state FROM table")
.filter(statement -> s.fetchSize(25));
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter { statement -> s.returnGeneratedValues("id") }
client.sql("SELECT id, name, state FROM table")
.filter { statement -> s.fetchSize(25) }
StatementFilterFunction
の実装により、Statement
のフィルタリングと Result
オブジェクトのフィルタリングが可能になります。
DatabaseClient
ベストプラクティス
DatabaseClient
クラスのインスタンスは、一度設定されるとスレッドセーフです。これは、DatabaseClient
の単一インスタンスを構成し、この共有参照を複数の DAO(またはリポジトリ)に安全に挿入できることを意味するため、重要です。DatabaseClient
は、ConnectionFactory
への参照を維持するという点でステートフルですが、この状態は会話状態ではありません。
DatabaseClient
クラスを使用する場合の一般的な方法は、Spring 構成ファイルで ConnectionFactory
を構成してから、その共有 ConnectionFactory
Bean を DAO クラスに依存性注入することです。DatabaseClient
は、ConnectionFactory
の setter で作成されます。これにより、次のような DAO が発生します。
Java
Kotlin
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory);
}
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao {
private val databaseClient = DatabaseClient.create(connectionFactory)
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
明示的な構成の代わりに、コンポーネントスキャンと依存性注入のアノテーションサポートを使用します。この場合、クラスに @Component
(コンポーネントスキャンの候補になります)でアノテーションを付け、ConnectionFactory
setter メソッドに @Autowired
でアノテーションを付けることができます。次の例は、その方法を示しています。
Java
Kotlin
@Component (1)
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
@Autowired (2)
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory); (3)
}
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 | @Component でクラスにアノテーションを付けます。 |
2 | ConnectionFactory setter メソッドに @Autowired でアノテーションを付けます。 |
3 | ConnectionFactory で新しい DatabaseClient を作成します。 |
@Component (1)
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { (2)
private val databaseClient = DatabaseClient(connectionFactory) (3)
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 | @Component でクラスにアノテーションを付けます。 |
2 | ConnectionFactory のコンストラクターインジェクション。 |
3 | ConnectionFactory で新しい DatabaseClient を作成します。 |
上記のテンプレート初期化スタイルのどちらを使用する(またはしない)かに関係なく、SQL を実行するたびに DatabaseClient
クラスの新しいインスタンスを作成する必要はほとんどありません。一度構成すると、DatabaseClient
インスタンスはスレッドセーフになります。アプリケーションが複数のデータベースにアクセスする場合、複数の DatabaseClient
インスタンスが必要になる場合があります。これには、複数の ConnectionFactory
が必要であり、その後、複数の異なる構成の DatabaseClient
インスタンスが必要です。
自動生成されたキーの取得
INSERT
ステートメントは、自動インクリメント列または ID 列を定義するテーブルに行を挿入するときにキーを生成する場合があります。生成する列名を完全に制御するには、目的の列に対して生成されたキーをリクエストする StatementFilterFunction
を登録するだけです。
Java
Kotlin
Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"))
.map(row -> row.get("id", Integer.class))
.first();
// generatedId emits the generated key once the INSERT statement has finished
val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter { statement -> s.returnGeneratedValues("id") }
.map { row -> row.get("id", Integer.class) }
.awaitOne()
// generatedId emits the generated key once the INSERT statement has finished
データベース接続の制御
このセクションでは以下について説明します。
ConnectionFactory
を使用する
Spring は、ConnectionFactory
を介してデータベースへの R2DBC 接続を取得します。ConnectionFactory
は R2DBC 仕様の一部であり、ドライバーの一般的なエントリポイントです。コンテナーまたはフレームワークで、接続プールとトランザクション管理の課題をアプリケーションコードから隠すことができます。開発者は、データベースへの接続方法に関する詳細を知る必要はありません。これは、ConnectionFactory
を設定する管理者の責任です。コードの開発とテストを行うときに両方のロールを果たす可能性がありますが、本番データソースがどのように構成されているかを必ずしも知っている必要はありません。
Spring の R2DBC レイヤーを使用すると、サードパーティが提供する接続プールの実装を使用して独自のレイヤーを構成できます。一般的な実装は R2DBC プール (r2dbc-pool
) です。Spring ディストリビューションでの実装は、テストのみを目的としており、プーリングは提供しません。
ConnectionFactory
を構成するには:
通常は R2DBC
ConnectionFactory
を取得するため、ConnectionFactory
との接続を取得します。R2DBC URL を提供します(正しい値については、ドライバーのドキュメントを参照してください)。
次の例は、ConnectionFactory
を構成する方法を示しています。
Java
Kotlin
ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
val factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
ConnectionFactoryUtils
を使用する
ConnectionFactoryUtils
クラスは、ConnectionFactory
から接続を取得し、(必要に応じて)接続を閉じるための static
メソッドを提供する便利で強力なヘルパークラスです。
サブスクライバー Context
-bound 接続、たとえば R2dbcTransactionManager
をサポートします。
SingleConnectionFactory
を使用する
SingleConnectionFactory
クラスは、使用するたびに閉じられない単一の Connection
をラップする DelegatingConnectionFactory
インターフェースの実装です。
(永続化ツールを使用する場合のように)プールされた接続を想定してクライアントコードが close
を呼び出す場合は、suppressClose
プロパティを true
に設定する必要があります。この設定は、物理接続をラップするクローズ抑制プロキシを返します。これをネイティブ Connection
または同様のオブジェクトにキャストすることはできなくなったことに注意してください。
SingleConnectionFactory
は主にテストクラスであり、R2DBC ドライバーで使用が許可されている場合は、パイプライン処理などの特定の要件に使用できます。プールされた ConnectionFactory
とは対照的に、常に同じ接続を再利用し、物理接続の過度の作成を回避します。
TransactionAwareConnectionFactoryProxy
を使用する
TransactionAwareConnectionFactoryProxy
は、ターゲット ConnectionFactory
のプロキシです。プロキシは、ConnectionFactory
をターゲットとしてラップし、Spring が管理するトランザクションの認識を追加します。
Spring の R2DBC サポートと統合されていない R2DBC クライアントを使用する場合は、このクラスを使用する必要があります。この場合でも、このクライアントを使用すると同時に、このクライアントを Spring 管理対象トランザクションに参加させることができます。リソース管理のために、R2DBC クライアントを ConnectionFactoryUtils への適切なアクセスと統合することが一般的に望ましいです。 |
詳細については、TransactionAwareConnectionFactoryProxy
javadoc を参照してください。
R2dbcTransactionManager
を使用する
R2dbcTransactionManager
クラスは、単一の R2DBC ConnectionFactory
の ReactiveTransactionManager
実装です。これは、指定された ConnectionFactory
からの R2DBC Connection
をサブスクライバー Context
にバインドし、潜在的に ConnectionFactory
ごとに 1 つのサブスクライバー Connection
を許可します。
R2DBC の標準 ConnectionFactory.create()
ではなく、R2DBC Connection
から ConnectionFactoryUtils.getConnection(ConnectionFactory)
を取得するには、アプリケーションコードが必要です。すべてのフレームワーククラス ( DatabaseClient
など) はこの戦略を暗黙的に使用します。トランザクションマネージャーで使用しない場合、検索戦略は ConnectionFactory.create()
とまったく同じように動作するため、どのような場合でも使用できます。