You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
774 lines
27 KiB
774 lines
27 KiB
[[r2dbc]] |
|
= Data Access with R2DBC |
|
|
|
https://r2dbc.io[R2DBC] ("Reactive Relational Database Connectivity") is a community-driven |
|
specification effort to standardize access to SQL databases using reactive patterns. |
|
|
|
|
|
[[r2dbc-packages]] |
|
== Package Hierarchy |
|
|
|
The Spring Framework's R2DBC abstraction framework consists of two different packages: |
|
|
|
* `core`: The `org.springframework.r2dbc.core` package contains the `DatabaseClient` |
|
class plus a variety of related classes. See |
|
xref:data-access/r2dbc.adoc#r2dbc-core[Using the R2DBC Core Classes to Control Basic R2DBC Processing and Error Handling]. |
|
|
|
* `connection`: The `org.springframework.r2dbc.connection` package contains a utility class |
|
for easy `ConnectionFactory` access and various simple `ConnectionFactory` implementations |
|
that you can use for testing and running unmodified R2DBC. See |
|
xref:data-access/r2dbc.adoc#r2dbc-connections[Controlling Database Connections]. |
|
|
|
|
|
[[r2dbc-core]] |
|
== Using the R2DBC Core Classes to Control Basic R2DBC Processing and Error Handling |
|
|
|
This section covers how to use the R2DBC core classes to control basic R2DBC processing, |
|
including error handling. It includes the following topics: |
|
|
|
* xref:data-access/r2dbc.adoc#r2dbc-DatabaseClient[Using `DatabaseClient`] |
|
* xref:data-access/r2dbc.adoc#r2dbc-DatabaseClient-examples-statement[Executing Statements] |
|
* xref:data-access/r2dbc.adoc#r2dbc-DatabaseClient-examples-query[Querying (`SELECT`)] |
|
* xref:data-access/r2dbc.adoc#r2dbc-DatabaseClient-examples-update[Updating (`INSERT`, `UPDATE`, and `DELETE`) with `DatabaseClient`] |
|
* xref:data-access/r2dbc.adoc#r2dbc-DatabaseClient-filter[Statement Filters] |
|
* xref:data-access/r2dbc.adoc#r2dbc-auto-generated-keys[Retrieving Auto-generated Keys] |
|
|
|
|
|
[[r2dbc-DatabaseClient]] |
|
=== Using `DatabaseClient` |
|
|
|
`DatabaseClient` is the central class in the R2DBC core package. It handles the |
|
creation and release of resources, which helps to avoid common errors, such as |
|
forgetting to close the connection. It performs the basic tasks of the core R2DBC |
|
workflow (such as statement creation and execution), leaving application code to provide |
|
SQL and extract results. The `DatabaseClient` class: |
|
|
|
* Runs SQL queries |
|
* Update statements and stored procedure calls |
|
* Performs iteration over `Result` instances |
|
* Catches R2DBC exceptions and translates them to the generic, more informative, |
|
exception hierarchy defined in the `org.springframework.dao` package. |
|
(See xref:data-access/dao.adoc#dao-exceptions[Consistent Exception Hierarchy].) |
|
|
|
The client has a functional, fluent API using reactive types for declarative composition. |
|
|
|
When you use the `DatabaseClient` for your code, you need only to implement |
|
`java.util.function` interfaces, giving them a clearly defined contract. |
|
Given a `Connection` provided by the `DatabaseClient` class, a `Function` |
|
callback creates a `Publisher`. The same is true for mapping functions that |
|
extract a `Row` result. |
|
|
|
You can use `DatabaseClient` within a DAO implementation through direct instantiation |
|
with a `ConnectionFactory` reference, or you can configure it in a Spring IoC container |
|
and give it to DAOs as a bean reference. |
|
|
|
The simplest way to create a `DatabaseClient` object is through a static factory method, as follows: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
DatabaseClient client = DatabaseClient.create(connectionFactory); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
val client = DatabaseClient.create(connectionFactory) |
|
---- |
|
====== |
|
|
|
NOTE: The `ConnectionFactory` should always be configured as a bean in the Spring IoC |
|
container. |
|
|
|
The preceding method creates a `DatabaseClient` with default settings. |
|
|
|
You can also obtain a `Builder` instance from `DatabaseClient.builder()`. |
|
You can customize the client by calling the following methods: |
|
|
|
* `….bindMarkers(…)`: Supply a specific `BindMarkersFactory` to configure named |
|
parameter to database bind marker translation. |
|
* `….executeFunction(…)`: Set the `ExecuteFunction` how `Statement` objects get |
|
run. |
|
* `….namedParameters(false)`: Disable named parameter expansion. Enabled by default. |
|
|
|
TIP: Dialects are resolved by {api-spring-framework}/r2dbc/core/binding/BindMarkersFactoryResolver.html[`BindMarkersFactoryResolver`] |
|
from a `ConnectionFactory`, typically by inspecting `ConnectionFactoryMetadata`. |
|
+ |
|
You can let Spring auto-discover your `BindMarkersFactory` by registering a |
|
class that implements `org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider` |
|
through `META-INF/spring.factories`. |
|
`BindMarkersFactoryResolver` discovers bind marker provider implementations from |
|
the class path using Spring's `SpringFactoriesLoader`. |
|
+ |
|
|
|
Currently supported databases are: |
|
|
|
* H2 |
|
* MariaDB |
|
* Microsoft SQL Server |
|
* MySQL |
|
* Postgres |
|
|
|
All SQL issued by this class is logged at the `DEBUG` level under the category |
|
corresponding to the fully qualified class name of the client instance (typically |
|
`DefaultDatabaseClient`). Additionally, each execution registers a checkpoint in |
|
the reactive sequence to aid debugging. |
|
|
|
The following sections provide some examples of `DatabaseClient` usage. These examples |
|
are not an exhaustive list of all of the functionality exposed by the `DatabaseClient`. |
|
See the attendant {api-spring-framework}/r2dbc/core/DatabaseClient.html[javadoc] for that. |
|
|
|
[[r2dbc-DatabaseClient-examples-statement]] |
|
==== Executing Statements |
|
|
|
`DatabaseClient` provides the basic functionality of running a statement. |
|
The following example shows what you need to include for minimal but fully functional |
|
code that creates a new table: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") |
|
.then(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") |
|
.await() |
|
---- |
|
====== |
|
|
|
`DatabaseClient` is designed for convenient, fluent usage. |
|
It exposes intermediate, continuation, and terminal methods at each stage of the |
|
execution specification. The preceding example above uses `then()` to return a completion |
|
`Publisher` that completes as soon as the query (or queries, if the SQL query contains |
|
multiple statements) completes. |
|
|
|
NOTE: `execute(…)` accepts either the SQL query string or a query `Supplier<String>` |
|
to defer the actual query creation until execution. |
|
|
|
[[r2dbc-DatabaseClient-examples-query]] |
|
==== Querying (`SELECT`) |
|
|
|
SQL queries can return values through `Row` objects or the number of affected rows. |
|
`DatabaseClient` can return the number of updated rows or the rows themselves, |
|
depending on the issued query. |
|
|
|
The following query gets the `id` and `name` columns from a table: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person") |
|
.fetch().first(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
val first = client.sql("SELECT id, name FROM person") |
|
.fetch().awaitSingle() |
|
---- |
|
====== |
|
|
|
The following query uses a bind variable: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") |
|
.bind("fn", "Joe") |
|
.fetch().first(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
val first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") |
|
.bind("fn", "Joe") |
|
.fetch().awaitSingle() |
|
---- |
|
====== |
|
|
|
You might have noticed the use of `fetch()` in the example above. `fetch()` is a |
|
continuation operator that lets you specify how much data you want to consume. |
|
|
|
Calling `first()` returns the first row from the result and discards remaining rows. |
|
You can consume data with the following operators: |
|
|
|
* `first()` return the first row of the entire result. Its Kotlin Coroutine variant |
|
is named `awaitSingle()` for non-nullable return values and `awaitSingleOrNull()` |
|
if the value is optional. |
|
* `one()` returns exactly one result and fails if the result contains more rows. |
|
Using Kotlin Coroutines, `awaitOne()` for exactly one value or `awaitOneOrNull()` |
|
if the value may be `null`. |
|
* `all()` returns all rows of the result. When using Kotlin Coroutines, use `flow()`. |
|
* `rowsUpdated()` returns the number of affected rows (`INSERT`/`UPDATE`/`DELETE` |
|
count). Its Kotlin Coroutine variant is named `awaitRowsUpdated()`. |
|
|
|
Without specifying further mapping details, queries return tabular results |
|
as `Map` whose keys are case-insensitive column names that map to their column value. |
|
|
|
You can take control over result mapping by supplying a `Function<Row, T>` that gets |
|
called for each `Row` so it can return arbitrary values (singular values, |
|
collections and maps, and objects). |
|
|
|
The following example extracts the `name` column and emits its value: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
Flux<String> names = client.sql("SELECT name FROM person") |
|
.map(row -> row.get("name", String.class)) |
|
.all(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
val names = client.sql("SELECT name FROM person") |
|
.map{ row: Row -> row.get("name", String.class) } |
|
.flow() |
|
---- |
|
====== |
|
|
|
Alternatively, there is a shortcut for mapping to a single value: |
|
|
|
[source,java] |
|
---- |
|
Flux<String> names = client.sql("SELECT name FROM person") |
|
.mapValue(String.class) |
|
.all(); |
|
---- |
|
|
|
Or you may map to a result object with bean properties or record components: |
|
|
|
[source,java] |
|
---- |
|
// assuming a name property on Person |
|
Flux<Person> persons = client.sql("SELECT name FROM person") |
|
.mapProperties(Person.class) |
|
.all(); |
|
---- |
|
|
|
[[r2dbc-DatabaseClient-mapping-null]] |
|
.What about `null`? |
|
**** |
|
Relational database results can contain `null` values. |
|
The Reactive Streams specification forbids the emission of `null` values. |
|
That requirement mandates proper `null` handling in the extractor function. |
|
While you can obtain `null` values from a `Row`, you must not emit a `null` |
|
value. You must wrap any `null` values in an object (for example, `Optional` |
|
for singular values) to make sure a `null` value is never returned directly |
|
by your extractor function. |
|
**** |
|
|
|
[[r2dbc-DatabaseClient-examples-update]] |
|
==== Updating (`INSERT`, `UPDATE`, and `DELETE`) with `DatabaseClient` |
|
|
|
The only difference of modifying statements is that these statements typically |
|
do not return tabular data so you use `rowsUpdated()` to consume results. |
|
|
|
The following example shows an `UPDATE` statement that returns the number |
|
of updated rows: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn") |
|
.bind("fn", "Joe") |
|
.fetch().rowsUpdated(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
val affectedRows = client.sql("UPDATE person SET first_name = :fn") |
|
.bind("fn", "Joe") |
|
.fetch().awaitRowsUpdated() |
|
---- |
|
====== |
|
|
|
[[r2dbc-DatabaseClient-named-parameters]] |
|
==== Binding Values to Queries |
|
|
|
A typical application requires parameterized SQL statements to select or |
|
update rows according to some input. These are typically `SELECT` statements |
|
constrained by a `WHERE` clause or `INSERT` and `UPDATE` statements that accept |
|
input parameters. Parameterized statements bear the risk of SQL injection if |
|
parameters are not escaped properly. `DatabaseClient` leverages R2DBC's |
|
`bind` API to eliminate the risk of SQL injection for query parameters. |
|
You can provide a parameterized SQL statement with the `execute(…)` operator |
|
and bind parameters to the actual `Statement`. Your R2DBC driver then runs |
|
the statement by using prepared statements and parameter substitution. |
|
|
|
Parameter binding supports two binding strategies: |
|
|
|
* By Index, using zero-based parameter indexes. |
|
* By Name, using the placeholder name. |
|
|
|
The following example shows parameter binding for a query: |
|
|
|
[source,java] |
|
---- |
|
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") |
|
.bind("id", "joe") |
|
.bind("name", "Joe") |
|
.bind("age", 34); |
|
---- |
|
|
|
Alternatively, you may pass in a map of names and values: |
|
|
|
[source,java] |
|
---- |
|
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); |
|
---- |
|
|
|
Or you may pass in a parameter object with bean properties or record components: |
|
|
|
[source,java] |
|
---- |
|
// 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); |
|
---- |
|
|
|
.R2DBC Native Bind Markers |
|
**** |
|
R2DBC uses database-native bind markers that depend on the actual database vendor. |
|
As an example, Postgres uses indexed markers, such as `$1`, `$2`, `$n`. |
|
Another example is SQL Server, which uses named bind markers prefixed with `@`. |
|
|
|
This is different from JDBC which requires `?` as bind markers. |
|
In JDBC, the actual drivers translate `?` bind markers to database-native |
|
markers as part of their statement execution. |
|
|
|
Spring Framework's R2DBC support lets you use native bind markers or named bind |
|
markers with the `:name` syntax. |
|
|
|
Named parameter support leverages a `BindMarkersFactory` instance to expand named |
|
parameters to native bind markers at the time of query execution, which gives you |
|
a certain degree of query portability across various database vendors. |
|
**** |
|
|
|
The query-preprocessor unrolls named `Collection` parameters into a series of bind |
|
markers to remove the need of dynamic query creation based on the number of arguments. |
|
Nested object arrays are expanded to allow usage of (for example) select lists. |
|
|
|
Consider the following query: |
|
|
|
[source,sql] |
|
---- |
|
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) |
|
---- |
|
|
|
The preceding query can be parameterized and run as follows: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
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); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
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) |
|
---- |
|
====== |
|
|
|
NOTE: Usage of select lists is vendor-dependent. |
|
|
|
The following example shows a simpler variant using `IN` predicates: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") |
|
.bind("ages", Arrays.asList(35, 50)); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
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 age IN (:ages)") |
|
.bind("tuples", arrayOf(35, 50)) |
|
---- |
|
====== |
|
|
|
NOTE: R2DBC itself does not support Collection-like values. Nevertheless, |
|
expanding a given `List` in the example above works for named parameters |
|
in Spring's R2DBC support, e.g. for use in `IN` clauses as shown above. |
|
However, inserting or updating array-typed columns (e.g. in Postgres) |
|
requires an array type that is supported by the underlying R2DBC driver: |
|
typically a Java array, e.g. `String[]` to update a `text[]` column. |
|
Do not pass `Collection<String>` or the like as an array parameter. |
|
|
|
[[r2dbc-DatabaseClient-filter]] |
|
==== Statement Filters |
|
|
|
Sometimes you need to fine-tune options on the actual `Statement` |
|
before it gets run. To do so, register a `Statement` filter |
|
(`StatementFilterFunction`) with the `DatabaseClient` to intercept and |
|
modify statements in their execution, as the following example shows: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") |
|
.filter((s, next) -> next.execute(s.returnGeneratedValues("id"))) |
|
.bind("name", …) |
|
.bind("state", …); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
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` also exposes a simplified `filter(…)` overload that accepts |
|
a `Function<Statement, Statement>`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
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)); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
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` implementations allow filtering of the |
|
`Statement` and filtering of `Result` objects. |
|
|
|
[[r2dbc-DatabaseClient-idioms]] |
|
==== `DatabaseClient` Best Practices |
|
|
|
Instances of the `DatabaseClient` class are thread-safe, once configured. This is |
|
important because it means that you can configure a single instance of a `DatabaseClient` |
|
and then safely inject this shared reference into multiple DAOs (or repositories). |
|
The `DatabaseClient` is stateful, in that it maintains a reference to a `ConnectionFactory`, |
|
but this state is not conversational state. |
|
|
|
A common practice when using the `DatabaseClient` class is to configure a `ConnectionFactory` |
|
in your Spring configuration file and then dependency-inject |
|
that shared `ConnectionFactory` bean into your DAO classes. The `DatabaseClient` is created in |
|
the setter for the `ConnectionFactory`. This leads to DAOs that resemble the following: |
|
|
|
-- |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
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... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { |
|
|
|
private val databaseClient = DatabaseClient.create(connectionFactory) |
|
|
|
// R2DBC-backed implementations of the methods on the CorporateEventDao follow... |
|
} |
|
---- |
|
====== |
|
-- |
|
|
|
An alternative to explicit configuration is to use component-scanning and annotation |
|
support for dependency injection. In this case, you can annotate the class with `@Component` |
|
(which makes it a candidate for component-scanning) and annotate the `ConnectionFactory` setter |
|
method with `@Autowired`. The following example shows how to do so: |
|
|
|
-- |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
@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> Annotate the class with `@Component`. |
|
<2> Annotate the `ConnectionFactory` setter method with `@Autowired`. |
|
<3> Create a new `DatabaseClient` with the `ConnectionFactory`. |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
@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> Annotate the class with `@Component`. |
|
<2> Constructor injection of the `ConnectionFactory`. |
|
<3> Create a new `DatabaseClient` with the `ConnectionFactory`. |
|
====== |
|
-- |
|
|
|
Regardless of which of the above template initialization styles you choose to use (or |
|
not), it is seldom necessary to create a new instance of a `DatabaseClient` class each |
|
time you want to run SQL. Once configured, a `DatabaseClient` instance is thread-safe. |
|
If your application accesses multiple |
|
databases, you may want multiple `DatabaseClient` instances, which requires multiple |
|
`ConnectionFactory` and, subsequently, multiple differently configured `DatabaseClient` |
|
instances. |
|
|
|
[[r2dbc-auto-generated-keys]] |
|
== Retrieving Auto-generated Keys |
|
|
|
`INSERT` statements may generate keys when inserting rows into a table |
|
that defines an auto-increment or identity column. To get full control over |
|
the column name to generate, simply register a `StatementFilterFunction` that |
|
requests the generated key for the desired column. |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
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 |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
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 |
|
---- |
|
====== |
|
|
|
|
|
[[r2dbc-connections]] |
|
== Controlling Database Connections |
|
|
|
This section covers: |
|
|
|
* xref:data-access/r2dbc.adoc#r2dbc-ConnectionFactory[Using `ConnectionFactory`] |
|
* xref:data-access/r2dbc.adoc#r2dbc-ConnectionFactoryUtils[Using `ConnectionFactoryUtils`] |
|
* xref:data-access/r2dbc.adoc#r2dbc-SingleConnectionFactory[Using `SingleConnectionFactory`] |
|
* xref:data-access/r2dbc.adoc#r2dbc-TransactionAwareConnectionFactoryProxy[Using `TransactionAwareConnectionFactoryProxy`] |
|
* xref:data-access/r2dbc.adoc#r2dbc-R2dbcTransactionManager[Using `R2dbcTransactionManager`] |
|
|
|
|
|
[[r2dbc-ConnectionFactory]] |
|
=== Using `ConnectionFactory` |
|
|
|
Spring obtains an R2DBC connection to the database through a `ConnectionFactory`. |
|
A `ConnectionFactory` is part of the R2DBC specification and is a common entry-point |
|
for drivers. It lets a container or a framework hide connection pooling |
|
and transaction management issues from the application code. As a developer, |
|
you need not know details about how to connect to the database. That is the |
|
responsibility of the administrator who sets up the `ConnectionFactory`. You |
|
most likely fill both roles as you develop and test code, but you do not |
|
necessarily have to know how the production data source is configured. |
|
|
|
When you use Spring's R2DBC layer, you can configure your own with a |
|
connection pool implementation provided by a third party. A popular |
|
implementation is R2DBC Pool (`r2dbc-pool`). Implementations in the Spring |
|
distribution are meant only for testing purposes and do not provide pooling. |
|
|
|
To configure a `ConnectionFactory`: |
|
|
|
. Obtain a connection with `ConnectionFactory` as you typically obtain an R2DBC `ConnectionFactory`. |
|
. Provide an R2DBC URL |
|
(See the documentation for your driver for the correct value). |
|
|
|
The following example shows how to configure a `ConnectionFactory`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
---- |
|
ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
---- |
|
val factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); |
|
---- |
|
====== |
|
|
|
|
|
[[r2dbc-ConnectionFactoryUtils]] |
|
=== Using `ConnectionFactoryUtils` |
|
|
|
The `ConnectionFactoryUtils` class is a convenient and powerful helper class |
|
that provides `static` methods to obtain connections from `ConnectionFactory` |
|
and close connections (if necessary). |
|
|
|
It supports subscriber ``Context``-bound connections with, for example |
|
`R2dbcTransactionManager`. |
|
|
|
|
|
[[r2dbc-SingleConnectionFactory]] |
|
=== Using `SingleConnectionFactory` |
|
|
|
The `SingleConnectionFactory` class is an implementation of `DelegatingConnectionFactory` |
|
interface that wraps a single `Connection` that is not closed after each use. |
|
|
|
If any client code calls `close` on the assumption of a pooled connection (as when using |
|
persistence tools), you should set the `suppressClose` property to `true`. This setting |
|
returns a close-suppressing proxy that wraps the physical connection. Note that you can |
|
no longer cast this to a native `Connection` or a similar object. |
|
|
|
`SingleConnectionFactory` is primarily a test class and may be used for specific requirements |
|
such as pipelining if your R2DBC driver permits for such use. |
|
In contrast to a pooled `ConnectionFactory`, it reuses the same connection all the time, avoiding |
|
excessive creation of physical connections. |
|
|
|
|
|
[[r2dbc-TransactionAwareConnectionFactoryProxy]] |
|
=== Using `TransactionAwareConnectionFactoryProxy` |
|
|
|
`TransactionAwareConnectionFactoryProxy` is a proxy for a target `ConnectionFactory`. |
|
The proxy wraps that target `ConnectionFactory` to add awareness of Spring-managed transactions. |
|
|
|
NOTE: Using this class is required if you use a R2DBC client that is not integrated otherwise |
|
with Spring's R2DBC support. In this case, you can still use this client and, at |
|
the same time, have this client participating in Spring managed transactions. It is generally |
|
preferable to integrate a R2DBC client with proper access to `ConnectionFactoryUtils` |
|
for resource management. |
|
|
|
See the {api-spring-framework}/r2dbc/connection/TransactionAwareConnectionFactoryProxy.html[`TransactionAwareConnectionFactoryProxy`] |
|
javadoc for more details. |
|
|
|
|
|
[[r2dbc-R2dbcTransactionManager]] |
|
=== Using `R2dbcTransactionManager` |
|
|
|
The `R2dbcTransactionManager` class is a `ReactiveTransactionManager` implementation for |
|
a single R2DBC `ConnectionFactory`. It binds an R2DBC `Connection` from the specified |
|
`ConnectionFactory` to the subscriber `Context`, potentially allowing for one subscriber |
|
`Connection` for each `ConnectionFactory`. |
|
|
|
Application code is required to retrieve the R2DBC `Connection` through |
|
`ConnectionFactoryUtils.getConnection(ConnectionFactory)`, instead of R2DBC's standard |
|
`ConnectionFactory.create()`. All framework classes (such as `DatabaseClient`) use this |
|
strategy implicitly. If not used with a transaction manager, the lookup strategy behaves |
|
exactly like `ConnectionFactory.create()` and can therefore be used in any case. |
|
|
|
|
|
|
|
|