mirror of
https://github.com/spring-projects/spring-data-relational.git
synced 2026-05-03 03:44:16 +01:00
#64 - Split documentation for R2DBC core into parts.
Original pull request: #106.
This commit is contained in:
committed by
Oliver Drotbohm
parent
fd4472aaaa
commit
ff69a41162
@@ -0,0 +1,241 @@
|
||||
The R2DBC support contains a wide range of features:
|
||||
|
||||
* Spring configuration support with Java-based `@Configuration` classes for an R2DBC driver instance.
|
||||
* `DatabaseClient` helper class that increases productivity when performing common R2DBC operations with integrated object mapping between rows and POJOs.
|
||||
* Exception translation into Spring's portable Data Access Exception hierarchy.
|
||||
* Feature-rich Object Mapping integrated with Spring's Conversion Service.
|
||||
* Annotation-based mapping metadata that is extensible to support other metadata formats.
|
||||
* Automatic implementation of Repository interfaces, including support for custom query methods.
|
||||
|
||||
For most tasks, you should use `DatabaseClient` or the Repository support, which both leverage the rich mapping functionality.
|
||||
`DatabaseClient` is the place to look for accessing functionality such as ad-hoc CRUD operations.
|
||||
|
||||
[[r2dbc.getting-started]]
|
||||
== Getting Started
|
||||
|
||||
An easy way to bootstrap setting up a working environment is to create a Spring-based project through https://start.spring.io[start.spring.io].
|
||||
|
||||
. Add the following to the pom.xml files `dependencies` element:
|
||||
+
|
||||
[source,xml,subs="+attributes"]
|
||||
----
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-bom</artifactId>
|
||||
<version>${r2dbc-releasetrain.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- other dependency elements omitted -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-r2dbc</artifactId>
|
||||
<version>{version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- a R2DBC driver -->
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-h2</artifactId>
|
||||
<version>{r2dbcVersion}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
----
|
||||
. Change the version of Spring in the pom.xml to be
|
||||
+
|
||||
[source,xml,subs="+attributes"]
|
||||
----
|
||||
<spring.framework.version>{springVersion}</spring.framework.version>
|
||||
----
|
||||
. Add the following location of the Spring Milestone repository for Maven to your `pom.xml` such that it is at the same level of your `<dependencies/>` element:
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestone</id>
|
||||
<name>Spring Maven MILESTONE Repository</name>
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
|
||||
The repository is also https://repo.spring.io/milestone/org/springframework/data/[browseable here].
|
||||
|
||||
You may also want to set the logging level to `DEBUG` to see some additional information. To do so, edit the `application.properties` file to have the following content:
|
||||
|
||||
[source]
|
||||
----
|
||||
logging.level.org.springframework.data.r2dbc=DEBUG
|
||||
----
|
||||
|
||||
Then you can create a `Person` class to persist:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.spring.r2dbc.example;
|
||||
|
||||
public class Person {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
public Person(String id, String name, int age) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Next, you need to create a table structure in your database:
|
||||
|
||||
[source,sql]
|
||||
----
|
||||
CREATE TABLE person
|
||||
(id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
age INT);
|
||||
----
|
||||
|
||||
You also need a main application to run:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.spring.r2dbc.example;
|
||||
|
||||
public class R2dbcApp {
|
||||
|
||||
private static final Log log = LogFactory.getLog(R2dbcApp.class);
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
ConnectionFactory connectionFactory = ConnectionFactories.get("rdbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
|
||||
|
||||
DatabaseClient client = DatabaseClient.create(connectionFactory);
|
||||
|
||||
client.execute()
|
||||
.sql("CREATE TABLE person" +
|
||||
"(id VARCHAR(255) PRIMARY KEY," +
|
||||
"name VARCHAR(255)," +
|
||||
"age INT)")
|
||||
.fetch()
|
||||
.rowsUpdated()
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
|
||||
client.insert()
|
||||
.into(Person.class)
|
||||
.using(new Person("joe", "Joe", 34))
|
||||
.then()
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
|
||||
client.select()
|
||||
.from(Person.class)
|
||||
.fetch()
|
||||
.first()
|
||||
.doOnNext(it -> log.info(it))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
When you run the main program, the preceding examples produce output similar to the following:
|
||||
|
||||
[source]
|
||||
----
|
||||
2018-11-28 10:47:03,893 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 310 - Executing SQL statement [CREATE TABLE person
|
||||
(id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
age INT)]
|
||||
2018-11-28 10:47:04,074 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 908 - Executing SQL statement [INSERT INTO person (id, name, age) VALUES($1, $2, $3)]
|
||||
2018-11-28 10:47:04,092 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 575 - Executing SQL statement [SELECT id, name, age FROM person]
|
||||
2018-11-28 10:47:04,436 INFO org.spring.r2dbc.example.R2dbcApp: 43 - Person [id='joe', name='Joe', age=34]
|
||||
----
|
||||
|
||||
Even in this simple example, there are few things to notice:
|
||||
|
||||
* You can create an instance of the central helper class in Spring Data R2DBC, <<r2dbc.datbaseclient,`DatabaseClient`>>, by using a standard `io.r2dbc.spi.ConnectionFactory` object.
|
||||
* The mapper works against standard POJO objects without the need for any additional metadata (though you can optionally provide that information. See <<mapping-chapter,here>>.).
|
||||
* Mapping conventions can use field access. Notice that the `Person` class has only getters.
|
||||
* If the constructor argument names match the column names of the stored row, they are used to instantiate the object.
|
||||
|
||||
[[r2dbc.examples-repo]]
|
||||
== Examples Repository
|
||||
|
||||
There is a https://github.com/spring-projects/spring-data-examples[GitHub repository with several examples] that you can download and play around with to get a feel for how the library works.
|
||||
|
||||
[[r2dbc.drivers]]
|
||||
== Connecting to a Relational Database with Spring
|
||||
|
||||
One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object using the IoC container. The following example explains Java-based configuration.
|
||||
|
||||
[[r2dbc.connectionfactory]]
|
||||
=== Registering a `ConnectionFactory` Instance using Java-based Metadata
|
||||
|
||||
The following example shows an example of using Java-based bean metadata to register an instance of a `io.r2dbc.spi.ConnectionFactory`:
|
||||
|
||||
.Registering a `io.r2dbc.spi.ConnectionFactory` object using Java-based bean metadata
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class ApplicationConfiguration extends AbstractR2dbcConfiguration {
|
||||
|
||||
@Override
|
||||
@Bean
|
||||
public ConnectionFactory connectionFactory() {
|
||||
return …;
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html[Spring's DAO support features].
|
||||
|
||||
`AbstractR2dbcConfiguration` registers also `DatabaseClient` that is required for database interaction and for Repository implementation.
|
||||
|
||||
[[r2dbc.drivers]]
|
||||
=== R2DBC Drivers
|
||||
|
||||
Spring Data R2DBC supports drivers by R2DBC's pluggable SPI mechanism. Any driver implementing the R2DBC spec can be used with Spring Data R2DBC.
|
||||
R2DBC is a relatively young initiative that gains significance by maturing through adoption.
|
||||
As of writing the following 3 drivers are available:
|
||||
|
||||
* https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`)
|
||||
* https://github.com/r2dbc/r2dbc-h2[H2] (`io.r2dbc:r2dbc-h2`)
|
||||
* https://github.com/r2dbc/r2dbc-mssql[Microsoft SQL Server] (`io.r2dbc:r2dbc-mssql`)
|
||||
* https://github.com/jasync-sql/jasync-sql[Microsoft SQL Server] (`com.github.jasync-sql:jasync-r2dbc-mysql`)
|
||||
|
||||
Spring Data R2DBC reacts to database specifics by inspecting `ConnectionFactoryMetadata` and selects the appropriate database dialect.
|
||||
You can configure an own `Dialect` if the used driver is not yet known to Spring Data R2DBC.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
[[r2dbc.datbaseclient]]
|
||||
= Introduction to `DatabaseClient`
|
||||
|
||||
Spring Data R2DBC includes a reactive, non-blocking `DatabaseClient` for database interaction. The client has a functional, fluent API with reactive types for declarative composition.
|
||||
`DatabaseClient` encapsulates resource handling such as opening and closing connections so your application code can make use of executing SQL queries or calling higher-level functionality such as inserting or selecting data.
|
||||
|
||||
NOTE: `DatabaseClient` is a young application component providing a minimal set of convenience methods that is likely to be extended through time.
|
||||
|
||||
NOTE: Once configured, `DatabaseClient` is thread-safe and can be reused across multiple instances.
|
||||
|
||||
Another central feature of `DatabaseClient` is translation of exceptions thrown by R2DBC drivers into Spring's portable Data Access Exception hierarchy. See "`<<r2dbc.exception>>`" for more information.
|
||||
|
||||
The next section contains an example of how to work with the `DatabaseClient` in the context of the Spring container.
|
||||
|
||||
[[r2dbc.datbaseclient.create]]
|
||||
== Creating `DatabaseClient`
|
||||
|
||||
The simplest way to create a `DatabaseClient` is through a static factory method:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
DatabaseClient.create(ConnectionFactory connectionFactory)
|
||||
----
|
||||
|
||||
The above method creates a `DatabaseClient` with default settings.
|
||||
|
||||
You can also use `DatabaseClient.builder()` with further options to customize the client:
|
||||
|
||||
* `exceptionTranslator`: Supply a specific `R2dbcExceptionTranslator` to customize how R2DBC exceptions are translated into Spring's portable Data Access Exception hierarchy. See "`<<r2dbc.exception>>`" for more information.
|
||||
* `dataAccessStrategy`: Strategy how SQL queries are generated and how objects are mapped.
|
||||
|
||||
Once built, a `DatabaseClient` instance is immutable. However, you can clone it and build a modified copy without affecting the original instance, as the following example shows:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
DatabaseClient client1 = DatabaseClient.builder()
|
||||
.exceptionTranslator(exceptionTranslatorA).build();
|
||||
|
||||
DatabaseClient client2 = client1.mutate()
|
||||
.exceptionTranslator(exceptionTranslatorB).build();
|
||||
----
|
||||
|
||||
== Controlling Database Connections
|
||||
|
||||
Spring Data R2DBC obtains a connection to the database through a `ConnectionFactory`.
|
||||
A `ConnectionFactory` is part of the R2DBC specification and is a generalized connection factory.
|
||||
It lets a container or a framework hide connection pooling and transaction management issues from the application code.
|
||||
|
||||
When you use Spring Data R2DBC, you can create a `ConnectionFactory` using your R2DBC driver.
|
||||
`ConnectionFactory` implementations can either return the same connection, different connections or provide connection pooling.
|
||||
`DatabaseClient` uses `ConnectionFactory` to create and release connections per operation without affinity to a particular connection across multiple operations.
|
||||
|
||||
[[r2dbc.exception]]
|
||||
= Exception Translation
|
||||
|
||||
The Spring framework provides exception translation for a wide variety of database and mapping technologies.
|
||||
The Spring support for R2DBC extends this feature by providing implementations of the `R2dbcExceptionTranslator` interface.
|
||||
|
||||
`R2dbcExceptionTranslator` is an interface to be implemented by classes that can translate between `R2dbcException` and Spring’s own `org.springframework.dao.DataAccessException`, which is agnostic in regard to data access strategy.
|
||||
Implementations can be generic (for example, using SQLState codes) or proprietary (for example, using Postgres error codes) for greater precision.
|
||||
|
||||
`R2dbcExceptionSubclassTranslator` is the implementation of `R2dbcExceptionTranslator` that is used by default.
|
||||
It considers R2DBC's categorized exception hierarchy to translate these into Spring's consistent exception hierarchy.
|
||||
`R2dbcExceptionSubclassTranslator` uses `SqlStateR2dbcExceptionTranslator` as fallback if it is not able to translate an exception.
|
||||
|
||||
`SqlErrorCodeR2dbcExceptionTranslator` uses specific vendor codes using Spring JDBC's `SQLErrorCodes`.
|
||||
It is more precise than the SQLState implementation.
|
||||
The error code translations are based on codes held in a JavaBean type class called `SQLErrorCodes`.
|
||||
This class is created and populated by an `SQLErrorCodesFactory`, which (as the name suggests) is a factory for creating SQLErrorCodes based on the contents of a configuration file named `sql-error-codes.xml` from Spring's Data Access module.
|
||||
This file is populated with vendor codes and based on the `ConnectionFactoryName` taken from `ConnectionFactoryMetadata`.
|
||||
The codes for the actual database you are using are used.
|
||||
|
||||
The `SqlErrorCodeR2dbcExceptionTranslator` applies matching rules in the following sequence:
|
||||
|
||||
1. Any custom translation implemented by a subclass. Normally, the provided concrete `SqlErrorCodeR2dbcExceptionTranslator` is used, so this rule does not apply. It applies only if you have actually provided a subclass implementation.
|
||||
2. Any custom implementation of the `SQLExceptionTranslator` interface that is provided as the `customSqlExceptionTranslator` property of the `SQLErrorCodes` class.
|
||||
3. Error code matching is applied.
|
||||
4. Use a fallback translator.
|
||||
|
||||
NOTE: The `SQLErrorCodesFactory` is used by default to define Error codes and custom exception translations. They are looked up in a file named `sql-error-codes.xml` from the classpath, and the matching `SQLErrorCodes` instance is located based on the database name from the database metadata of the database in use. `SQLErrorCodesFactory` requires Spring JDBC.
|
||||
|
||||
You can extend `SqlErrorCodeR2dbcExceptionTranslator`, as the following example shows:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class CustomSqlErrorCodeR2dbcExceptionTranslator extends SqlErrorCodeR2dbcExceptionTranslator {
|
||||
|
||||
protected DataAccessException customTranslate(String task, String sql, R2dbcException r2dbcex) {
|
||||
if (sqlex.getErrorCode() == -12345) {
|
||||
return new DeadlockLoserDataAccessException(task, r2dbcex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In the preceding example, the specific error code (`-12345`) is translated, while other errors are left to be translated by the default translator implementation.
|
||||
To use this custom translator, you must configure `DatabaseClient` through the builder method `exceptionTranslator`, and you must use this `DatabaseClient` for all of the data access processing where this translator is needed.
|
||||
The following example shows how you can use this custom translator:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
ConnectionFactory connectionFactory = …;
|
||||
|
||||
CustomSqlErrorCodeR2dbcExceptionTranslator exceptionTranslator = new CustomSqlErrorCodeR2dbcExceptionTranslator();
|
||||
|
||||
DatabaseClient client = DatabaseClient.builder()
|
||||
.connectionFactory(connectionFactory)
|
||||
.exceptionTranslator(exceptionTranslator)
|
||||
.build();
|
||||
----
|
||||
@@ -0,0 +1,163 @@
|
||||
[[r2dbc.datbaseclient.statements]]
|
||||
= Running Statements
|
||||
|
||||
Running a statement is the basic functionality that is covered by `DatabaseClient`.
|
||||
The following example shows what you need to include for a minimal but fully functional class that creates a new table:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Mono<Void> completion = client.execute()
|
||||
.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
|
||||
.then();
|
||||
----
|
||||
|
||||
`DatabaseClient` is designed for a convenient fluent usage.
|
||||
It exposes intermediate, continuation, and terminal methods at each stage of the execution specification.
|
||||
The 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().sql(…)` accepts either the SQL query string or a query `Supplier<String>` to defer the actual query creation until execution.
|
||||
|
||||
[[r2dbc.datbaseclient.queries]]
|
||||
== Running Queries
|
||||
|
||||
SQL queries can return values 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 example shows an `UPDATE` statement that returns the number of updated rows:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Mono<Integer> affectedRows = client.execute()
|
||||
.sql("UPDATE person SET name = 'Joe'")
|
||||
.fetch().rowsUpdated();
|
||||
----
|
||||
|
||||
Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumes by streaming each `Row`.
|
||||
You might have noticed the use of `fetch()` in the previous example.
|
||||
`fetch()` is a continuation operator that allows you to specify how much data you want to consume.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Mono<Map<String, Object>> first = client.execute()
|
||||
.sql("SELECT id, name FROM person")
|
||||
.fetch().first();
|
||||
----
|
||||
|
||||
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
|
||||
* `one()` returns exactly one result and fails if the result contains more rows.
|
||||
* `all()` returns all rows of the result
|
||||
* `rowsUpdated()` returns the number of affected rows (`INSERT` count, `UPDATE` count)
|
||||
|
||||
`DatabaseClient` queries return their results by default as `Map` of column name to value. You can customize type mapping by applying an `as(Class<T>)` operator.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Flux<Person> all = client.execute()
|
||||
.sql("SELECT id, name FROM mytable")
|
||||
.as(Person.class)
|
||||
.fetch().all();
|
||||
----
|
||||
|
||||
`as(…)` applies <<mapping-conventions,Convention-based Object Mapping>> and maps the resulting columns to your POJO.
|
||||
|
||||
[[r2dbc.datbaseclient.mapping]]
|
||||
== Mapping Results
|
||||
|
||||
You can customize result extraction beyond `Map` and POJO result extraction by providing an extractor `BiFunction<Row, RowMetadata, T>`.
|
||||
The extractor function interacts directly with R2DBC's `Row` and `RowMetadata` objects and can return arbitrary values (singular values, collections/maps, objects).
|
||||
|
||||
The following example extracts the `id` column and emits its value:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Flux<String> names= client.execute()
|
||||
.sql("SELECT name FROM person")
|
||||
.map((row, rowMetadata) -> row.get("id", String.class))
|
||||
.all();
|
||||
----
|
||||
|
||||
[[r2dbc.datbaseclient.mapping.null]]
|
||||
.What about `null`?
|
||||
****
|
||||
Relational database results may contain `null` values.
|
||||
Reactive Streams forbids emission of `null` values which requires a 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 (e.g. `Optional` for singular values) to make sure a `null` value is never returned directly by your extractor function.
|
||||
****
|
||||
|
||||
[[r2dbc.datbaseclient.binding]]
|
||||
== 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`/`UPDATE` statements accepting 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 `sql(…)` operator and bind parameters to the actual `Statement`.
|
||||
Your R2DBC driver then executes the statement using prepared statements and parameter substitution.
|
||||
|
||||
Parameter binding supports various 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.execute()
|
||||
.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bind("id", "joe")
|
||||
.bind("name", "Joe")
|
||||
.bind("age", 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 that uses named bind markers prefixed with `@` (at).
|
||||
|
||||
This is different from JDBC which requires `?` (question mark) as bind markers.
|
||||
In JDBC, the actual drivers translate question mark bind markers to database-native markers as part of their statement execution.
|
||||
|
||||
Spring Data R2DBC allows you to use native bind markers or named bind markers with the `:name` syntax.
|
||||
|
||||
Named parameter support leverages ``Dialect``s 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 e.g. select lists.
|
||||
|
||||
Consider the following query:
|
||||
|
||||
[source,sql]
|
||||
----
|
||||
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
|
||||
----
|
||||
|
||||
This query can be parametrized and executed as:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
List<Object[]> tuples = new ArrayList<>();
|
||||
tuples.add(new Object[] {"John", 35});
|
||||
tuples.add(new Object[] {"Ann", 50});
|
||||
|
||||
db.execute()
|
||||
.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
|
||||
.bind("tuples", tuples);
|
||||
----
|
||||
|
||||
NOTE: Usage of select lists is vendor-dependent.
|
||||
|
||||
A simpler variant using `IN` predicates:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
db.execute()
|
||||
.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
|
||||
.bind("ages", Arrays.asList(35, 50));
|
||||
----
|
||||
@@ -0,0 +1,29 @@
|
||||
[[r2dbc.datbaseclient.transactions]]
|
||||
== Transactions
|
||||
|
||||
A common pattern when using relational databases is grouping multiple queries within a unit of work that is guarded by a transaction.
|
||||
Relational databases typically associate a transaction with a single transport connection.
|
||||
Using different connections hence results in utilizing different transactions.
|
||||
Spring Data R2DBC includes a transactional `DatabaseClient` implementation with `TransactionalDatabaseClient` that allows you to group multiple statements within the same transaction.
|
||||
`TransactionalDatabaseClient` is a extension of `DatabaseClient` that exposes the same functionality as `DatabaseClient` and adds transaction-management methods.
|
||||
|
||||
You can run multiple statements within a transaction using the `inTransaction(Function)` closure:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
|
||||
|
||||
Flux<Void> completion = databaseClient.inTransaction(db -> {
|
||||
|
||||
return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bind("id", "joe")
|
||||
.bind("name", "Joe")
|
||||
.bind("age", 34)
|
||||
.fetch().rowsUpdated()
|
||||
.then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)")
|
||||
.bind("id", "joe")
|
||||
.bind("name", "Joe")
|
||||
.fetch().rowsUpdated())
|
||||
.then();
|
||||
});
|
||||
----
|
||||
@@ -1,536 +1,10 @@
|
||||
[[r2dbc.core]]
|
||||
= R2DBC support
|
||||
|
||||
The R2DBC support contains a wide range of features:
|
||||
include::r2dbc-core.adoc[]
|
||||
|
||||
* Spring configuration support with Java-based `@Configuration` classes for an R2DBC driver instance.
|
||||
* `DatabaseClient` helper class that increases productivity when performing common R2DBC operations with integrated object mapping between rows and POJOs.
|
||||
* Exception translation into Spring's portable Data Access Exception hierarchy.
|
||||
* Feature-rich Object Mapping integrated with Spring's Conversion Service.
|
||||
* Annotation-based mapping metadata that is extensible to support other metadata formats.
|
||||
* Automatic implementation of Repository interfaces, including support for custom query methods.
|
||||
include::r2dbc-databaseclient.adoc[leveloffset=+1]
|
||||
|
||||
For most tasks, you should use `DatabaseClient` or the Repository support, which both leverage the rich mapping functionality.
|
||||
`DatabaseClient` is the place to look for accessing functionality such as ad-hoc CRUD operations.
|
||||
include::r2dbc-sql.adoc[leveloffset=+1]
|
||||
|
||||
[[r2dbc.getting-started]]
|
||||
== Getting Started
|
||||
|
||||
An easy way to bootstrap setting up a working environment is to create a Spring-based project through https://start.spring.io[start.spring.io].
|
||||
|
||||
. Add the following to the pom.xml files `dependencies` element:
|
||||
+
|
||||
[source,xml,subs="+attributes"]
|
||||
----
|
||||
<dependencies>
|
||||
|
||||
<!-- other dependency elements omitted -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-r2dbc</artifactId>
|
||||
<version>{version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- a R2DBC driver -->
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-h2</artifactId>
|
||||
<version>{r2dbcVersion}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
----
|
||||
. Change the version of Spring in the pom.xml to be
|
||||
+
|
||||
[source,xml,subs="+attributes"]
|
||||
----
|
||||
<spring.framework.version>{springVersion}</spring.framework.version>
|
||||
----
|
||||
. Add the following location of the Spring Milestone repository for Maven to your `pom.xml` such that it is at the same level of your `<dependencies/>` element:
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestone</id>
|
||||
<name>Spring Maven MILESTONE Repository</name>
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
|
||||
The repository is also https://repo.spring.io/milestone/org/springframework/data/[browseable here].
|
||||
|
||||
You may also want to set the logging level to `DEBUG` to see some additional information. To do so, edit the `application.properties` file to have the following content:
|
||||
|
||||
[source]
|
||||
----
|
||||
logging.level.org.springframework.data.r2dbc=DEBUG
|
||||
----
|
||||
|
||||
Then you can create a `Person` class to persist:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.spring.r2dbc.example;
|
||||
|
||||
public class Person {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
public Person(String id, String name, int age) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Next, you need to create a table structure in your database:
|
||||
|
||||
[source,sql]
|
||||
----
|
||||
CREATE TABLE person
|
||||
(id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
age INT);
|
||||
----
|
||||
|
||||
You also need a main application to run:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.spring.r2dbc.example;
|
||||
|
||||
public class R2dbcApp {
|
||||
|
||||
private static final Log log = LogFactory.getLog(R2dbcApp.class);
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
ConnectionFactory connectionFactory = new H2ConnectionFactory(H2ConnectionConfiguration.builder()
|
||||
.url("mem:test;DB_CLOSE_DELAY=10")
|
||||
.build());
|
||||
|
||||
DatabaseClient client = DatabaseClient.create(connectionFactory);
|
||||
|
||||
client.execute()
|
||||
.sql("CREATE TABLE person" +
|
||||
"(id VARCHAR(255) PRIMARY KEY," +
|
||||
"name VARCHAR(255)," +
|
||||
"age INT)")
|
||||
.fetch()
|
||||
.rowsUpdated()
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
|
||||
client.insert()
|
||||
.into(Person.class)
|
||||
.using(new Person("joe", "Joe", 34))
|
||||
.then()
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
|
||||
client.select()
|
||||
.from(Person.class)
|
||||
.fetch()
|
||||
.first()
|
||||
.doOnNext(it -> log.info(it))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
When you run the main program, the preceding examples produce output similar to the following:
|
||||
|
||||
[source]
|
||||
----
|
||||
2018-11-28 10:47:03,893 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 310 - Executing SQL statement [CREATE TABLE person
|
||||
(id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
age INT)]
|
||||
2018-11-28 10:47:04,074 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 908 - Executing SQL statement [INSERT INTO person (id, name, age) VALUES($1, $2, $3)]
|
||||
2018-11-28 10:47:04,092 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 575 - Executing SQL statement [SELECT id, name, age FROM person]
|
||||
2018-11-28 10:47:04,436 INFO org.spring.r2dbc.example.R2dbcApp: 43 - Person [id='joe', name='Joe', age=34]
|
||||
----
|
||||
|
||||
Even in this simple example, there are few things to notice:
|
||||
|
||||
* You can create an instance of the central helper class in Spring Data R2DBC, <<r2dbc.datbaseclient,`DatabaseClient`>>, by using a standard `io.r2dbc.spi.ConnectionFactory` object.
|
||||
* The mapper works against standard POJO objects without the need for any additional metadata (though you can optionally provide that information. See <<mapping-chapter,here>>.).
|
||||
* Mapping conventions can use field access. Notice that the `Person` class has only getters.
|
||||
* If the constructor argument names match the column names of the stored row, they are used to instantiate the object.
|
||||
|
||||
[[r2dbc.examples-repo]]
|
||||
== Examples Repository
|
||||
|
||||
There is a https://github.com/spring-projects/spring-data-examples[GitHub repository with several examples] that you can download and play around with to get a feel for how the library works.
|
||||
|
||||
[[r2dbc.drivers]]
|
||||
== Connecting to a Relational Database with Spring
|
||||
|
||||
One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object using the IoC container. The following example explains Java-based configuration.
|
||||
|
||||
[[r2dbc.connectionfactory]]
|
||||
=== Registering a `ConnectionFactory` Instance using Java-based Metadata
|
||||
|
||||
The following example shows an example of using Java-based bean metadata to register an instance of a `io.r2dbc.spi.ConnectionFactory`:
|
||||
|
||||
.Registering a `io.r2dbc.spi.ConnectionFactory` object using Java-based bean metadata
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class ApplicationConfiguration extends AbstractR2dbcConfiguration {
|
||||
|
||||
@Override
|
||||
@Bean
|
||||
public ConnectionFactory connectionFactory() {
|
||||
return …;
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html[Spring's DAO support features].
|
||||
|
||||
`AbstractR2dbcConfiguration` registers also `DatabaseClient` that is required for database interaction and for Repository implementation.
|
||||
|
||||
[[r2dbc.drivers]]
|
||||
=== R2DBC Drivers
|
||||
|
||||
Spring Data R2DBC supports drivers by R2DBC's pluggable SPI mechanism. Any driver implementing the R2DBC spec can be used with Spring Data R2DBC.
|
||||
R2DBC is a relatively young initiative that gains significance by maturing through adoption.
|
||||
As of writing the following 3 drivers are available:
|
||||
|
||||
* https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`)
|
||||
* https://github.com/r2dbc/r2dbc-h2[H2] (`io.r2dbc:r2dbc-h2`)
|
||||
* https://github.com/r2dbc/r2dbc-mssql[Microsoft SQL Server] (`io.r2dbc:r2dbc-mssql`)
|
||||
|
||||
Spring Data R2DBC reacts to database specifics by inspecting `ConnectionFactoryMetadata` and selects the appropriate database dialect.
|
||||
You can configure an own `Dialect` if the used driver is not yet known to Spring Data R2DBC.
|
||||
|
||||
[[r2dbc.datbaseclient]]
|
||||
== Introduction to `DatabaseClient`
|
||||
|
||||
Spring Data R2DBC includes a reactive, non-blocking `DatabaseClient` for database interaction. The client has a functional, fluent API with reactive types for declarative composition.
|
||||
`DatabaseClient` encapsulates resource handling such as opening and closing connections so your application code can make use of executing SQL queries or calling higher-level functionality such as inserting or selecting data.
|
||||
|
||||
NOTE: `DatabaseClient` is a young application component providing a minimal set of convenience methods that is likely to be extended through time.
|
||||
|
||||
NOTE: Once configured, `DatabaseClient` is thread-safe and can be reused across multiple instances.
|
||||
|
||||
Another central feature of `DatabaseClient` is translation of exceptions thrown by R2DBC drivers into Spring's portable Data Access Exception hierarchy. See "`<<r2dbc.exception>>`" for more information.
|
||||
|
||||
The next section contains an example of how to work with the `DatabaseClient` in the context of the Spring container.
|
||||
|
||||
[[r2dbc.datbaseclient.create]]
|
||||
=== Creating `DatabaseClient`
|
||||
|
||||
The simplest way to create a `DatabaseClient` is through a static factory method:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
DatabaseClient.create(ConnectionFactory connectionFactory)
|
||||
----
|
||||
|
||||
The above method creates a `DatabaseClient` with default settings.
|
||||
|
||||
You can also use `DatabaseClient.builder()` with further options to customize the client:
|
||||
|
||||
* `exceptionTranslator`: Supply a specific `R2dbcExceptionTranslator` to customize how R2DBC exceptions are translated into Spring's portable Data Access Exception hierarchy. See "`<<r2dbc.exception>>`" for more information.
|
||||
* `dataAccessStrategy`: Strategy how SQL queries are generated and how objects are mapped.
|
||||
|
||||
Once built, a `DatabaseClient` instance is immutable. However, you can clone it and build a modified copy without affecting the original instance, as the following example shows:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
DatabaseClient client1 = DatabaseClient.builder()
|
||||
.exceptionTranslator(exceptionTranslatorA).build();
|
||||
|
||||
DatabaseClient client2 = client1.mutate()
|
||||
.exceptionTranslator(exceptionTranslatorB).build();
|
||||
----
|
||||
|
||||
=== Controlling Database Connections
|
||||
|
||||
Spring Data R2DBC obtains a connection to the database through a `ConnectionFactory`.
|
||||
A `ConnectionFactory` is part of the R2DBC specification and is a generalized connection factory.
|
||||
It lets a container or a framework hide connection pooling and transaction management issues from the application code.
|
||||
|
||||
When you use Spring Data R2DBC, you can create a `ConnectionFactory` using your R2DBC driver.
|
||||
`ConnectionFactory` implementations can either return the same connection, different connections or provide connection pooling.
|
||||
`DatabaseClient` uses `ConnectionFactory` to create and release connections per operation without affinity to a particular connection across multiple operations.
|
||||
|
||||
[[r2dbc.exception]]
|
||||
== Exception Translation
|
||||
|
||||
The Spring framework provides exception translation for a wide variety of database and mapping technologies.
|
||||
This has traditionally been for JDBC and JPA. The Spring support for R2DBC extends this feature by providing implementations of the `R2dbcExceptionTranslator` interface.
|
||||
|
||||
`R2dbcExceptionTranslator` is an interface to be implemented by classes that can translate between `R2dbcException` and Spring’s own `org.springframework.dao.DataAccessException`, which is agnostic in regard to data access strategy.
|
||||
Implementations can be generic (for example, using SQLState codes) or proprietary (for example, using Postgres error codes) for greater precision.
|
||||
|
||||
`SqlErrorCodeR2dbcExceptionTranslator` is the implementation of `R2dbcExceptionTranslator` that is used by default.
|
||||
This implementation uses specific vendor codes.
|
||||
It is more precise than the SQLState implementation.
|
||||
The error code translations are based on codes held in a JavaBean type class called `SQLErrorCodes`.
|
||||
This class is created and populated by an `SQLErrorCodesFactory`, which (as the name suggests) is a factory for creating SQLErrorCodes based on the contents of a configuration file named `sql-error-codes.xml` from Spring's Data Access module.
|
||||
This file is populated with vendor codes and based on the `ConnectionFactoryName` taken from `ConnectionFactoryMetadata`.
|
||||
The codes for the actual database you are using are used.
|
||||
|
||||
The `SqlErrorCodeR2dbcExceptionTranslator` applies matching rules in the following sequence:
|
||||
|
||||
1. Any custom translation implemented by a subclass. Normally, the provided concrete `SqlErrorCodeR2dbcExceptionTranslator` is used, so this rule does not apply. It applies only if you have actually provided a subclass implementation.
|
||||
2. Any custom implementation of the `SQLExceptionTranslator` interface that is provided as the `customSqlExceptionTranslator` property of the `SQLErrorCodes` class.
|
||||
3. Error code matching is applied.
|
||||
4. Use a fallback translator.
|
||||
|
||||
|
||||
NOTE: The `SQLErrorCodesFactory` is used by default to define Error codes and custom exception translations. They are looked up in a file named `sql-error-codes.xml` from the classpath, and the matching `SQLErrorCodes` instance is located based on the database name from the database metadata of the database in use. `SQLErrorCodesFactory` is as of now part of Spring JDBC. Spring Data R2DBC reuses existing translation configurations.
|
||||
|
||||
You can extend `SqlErrorCodeR2dbcExceptionTranslator`, as the following example shows:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class CustomSqlErrorCodeR2dbcExceptionTranslator extends SqlErrorCodeR2dbcExceptionTranslator {
|
||||
|
||||
protected DataAccessException customTranslate(String task, String sql, R2dbcException r2dbcex) {
|
||||
if (sqlex.getErrorCode() == -12345) {
|
||||
return new DeadlockLoserDataAccessException(task, r2dbcex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In the preceding example, the specific error code (`-12345`) is translated, while other errors are left to be translated by the default translator implementation.
|
||||
To use this custom translator, you must configure `DatabaseClient` through the builder method `exceptionTranslator`, and you must use this `DatabaseClient` for all of the data access processing where this translator is needed.
|
||||
The following example shows how you can use this custom translator:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
ConnectionFactory connectionFactory = …;
|
||||
|
||||
CustomSqlErrorCodeR2dbcExceptionTranslator exceptionTranslator = new CustomSqlErrorCodeR2dbcExceptionTranslator();
|
||||
|
||||
DatabaseClient client = DatabaseClient.builder()
|
||||
.connectionFactory(connectionFactory)
|
||||
.exceptionTranslator(exceptionTranslator)
|
||||
.build();
|
||||
----
|
||||
|
||||
[[r2dbc.datbaseclient.statements]]
|
||||
=== Running Statements
|
||||
|
||||
Running a statement is the basic functionality that is covered by `DatabaseClient`.
|
||||
The following example shows what you need to include for a minimal but fully functional class that creates a new table:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Mono<Void> completion = client.execute()
|
||||
.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
|
||||
.then();
|
||||
----
|
||||
|
||||
`DatabaseClient` is designed for a convenient fluent usage.
|
||||
It exposes intermediate, continuation, and terminal methods at each stage of the execution specification.
|
||||
The 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().sql(…)` accepts either the SQL query string or a query `Supplier<String>` to defer the actual query creation until execution.
|
||||
|
||||
[[r2dbc.datbaseclient.queries]]
|
||||
=== Running Queries
|
||||
|
||||
SQL queries can return values 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 example shows an `UPDATE` statement that returns the number of updated rows:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Mono<Integer> affectedRows = client.execute()
|
||||
.sql("UPDATE person SET name = 'Joe'")
|
||||
.fetch().rowsUpdated();
|
||||
----
|
||||
|
||||
Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumes by streaming each `Row`.
|
||||
You might have noticed the use of `fetch()` in the previous example.
|
||||
`fetch()` is a continuation operator that allows you to specify how much data you want to consume.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Mono<Map<String, Object>> first = client.execute()
|
||||
.sql("SELECT id, name FROM person")
|
||||
.fetch().first();
|
||||
----
|
||||
|
||||
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
|
||||
* `one()` returns exactly one result and fails if the result contains more rows.
|
||||
* `all()` returns all rows of the result
|
||||
* `rowsUpdated()` returns the number of affected rows (`INSERT` count, `UPDATE` count)
|
||||
|
||||
`DatabaseClient` queries return their results by default as `Map` of column name to value. You can customize type mapping by applying an `as(Class<T>)` operator.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Flux<Person> all = client.execute()
|
||||
.sql("SELECT id, name FROM mytable")
|
||||
.as(Person.class)
|
||||
.fetch().all();
|
||||
----
|
||||
|
||||
`as(…)` applies <<mapping-conventions,Convention-based Object Mapping>> and maps the resulting columns to your POJO.
|
||||
|
||||
[[r2dbc.datbaseclient.mapping]]
|
||||
=== Mapping Results
|
||||
|
||||
You can customize result extraction beyond `Map` and POJO result extraction by providing an extractor `BiFunction<Row, RowMetadata, T>`.
|
||||
The extractor function interacts directly with R2DBC's `Row` and `RowMetadata` objects and can return arbitrary values (singular values, collections/maps, objects).
|
||||
|
||||
The following example extracts the `id` column and emits its value:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Flux<String> names= client.execute()
|
||||
.sql("SELECT name FROM person")
|
||||
.map((row, rowMetadata) -> row.get("id", String.class))
|
||||
.all();
|
||||
----
|
||||
|
||||
[[r2dbc.datbaseclient.mapping.null]]
|
||||
.What about `null`?
|
||||
****
|
||||
Relational database results may contain `null` values.
|
||||
Reactive Streams forbids emission of `null` values which requires a 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 (e.g. `Optional` for singular values) to make sure a `null` value is never returned directly by your extractor function.
|
||||
****
|
||||
|
||||
[[r2dbc.datbaseclient.binding]]
|
||||
=== 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`/`UPDATE` statements accepting 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 `sql(…)` operator and bind parameters to the actual `Statement`.
|
||||
Your R2DBC driver then executes the statement using prepared statements and parameter substitution.
|
||||
|
||||
Parameter binding supports various 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.execute()
|
||||
.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bind("id", "joe")
|
||||
.bind("name", "Joe")
|
||||
.bind("age", 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 that uses named bind markers prefixed with `@` (at).
|
||||
|
||||
This is different from JDBC which requires `?` (question mark) as bind markers.
|
||||
In JDBC, the actual drivers translate question mark bind markers to database-native markers as part of their statement execution.
|
||||
|
||||
Spring Data R2DBC allows you to use native bind markers or named bind markers with the `:name` syntax.
|
||||
|
||||
Named parameter support leverages ``Dialect``s 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 e.g. select lists.
|
||||
|
||||
Consider the following query:
|
||||
|
||||
[source,sql]
|
||||
----
|
||||
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
|
||||
----
|
||||
|
||||
This query can be parametrized and executed as:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
List<Object[]> tuples = new ArrayList<>();
|
||||
tuples.add(new Object[] {"John", 35});
|
||||
tuples.add(new Object[] {"Ann", 50});
|
||||
|
||||
db.execute()
|
||||
.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
|
||||
.bind("tuples", tuples);
|
||||
----
|
||||
|
||||
NOTE: Usage of select lists is vendor-dependent.
|
||||
|
||||
A simpler variant using `IN` predicates:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
db.execute()
|
||||
.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
|
||||
.bind("ages", Arrays.asList(35, 50));
|
||||
----
|
||||
|
||||
[[r2dbc.datbaseclient.transactions]]
|
||||
=== Transactions
|
||||
|
||||
A common pattern when using relational databases is grouping multiple queries within a unit of work that is guarded by a transaction.
|
||||
Relational databases typically associate a transaction with a single transport connection.
|
||||
Using different connections hence results in utilizing different transactions.
|
||||
Spring Data R2DBC includes a transactional `DatabaseClient` implementation with `TransactionalDatabaseClient` that allows you to group multiple statements within the same transaction.
|
||||
`TransactionalDatabaseClient` is a extension of `DatabaseClient` that exposes the same functionality as `DatabaseClient` and adds transaction-management methods.
|
||||
|
||||
You can run multiple statements within a transaction using the `inTransaction(Function)` closure:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
|
||||
|
||||
Flux<Void> completion = databaseClient.inTransaction(db -> {
|
||||
|
||||
return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bind("id", "joe")
|
||||
.bind("name", "Joe")
|
||||
.bind("age", 34)
|
||||
.fetch().rowsUpdated()
|
||||
.then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)")
|
||||
.bind("id", "joe")
|
||||
.bind("name", "Joe")
|
||||
.fetch().rowsUpdated())
|
||||
.then();
|
||||
});
|
||||
----
|
||||
include::r2dbc-transactions.adoc[leveloffset=+1]
|
||||
|
||||
Reference in New Issue
Block a user