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:
Mono<Void> completion = client.execute("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.then();
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.
|
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:
Mono<Integer> affectedRows = client.execute("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 consumed by streaming each Row
.
You might have noticed the use of fetch()
in the previous example.
fetch()
is a continuation operator that lets you specify how much data you want to consume.
Mono<Map<String, Object>> first = client.execute("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).
By default, DatabaseClient
queries return their results as Map
of column name to value.
You can customize type mapping by applying an as(Class<T>)
operator, as follows:
Flux<Person> all = client.execute("SELECT id, name FROM mytable")
.as(Person.class)
.fetch().all();
as(…)
applies Convention-based Object Mapping and maps the resulting columns to your POJO.
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 and maps, and objects).
The following example extracts the id
column and emits its value:
Flux<String> names = client.execute("SELECT name FROM person")
.map((row, rowMetadata) -> row.get("id", String.class))
.all();
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.
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 executes 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:
db.execute("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
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 Data R2DBC lets you use native bind markers or named bind markers with the :name
syntax.
Named parameter support leverages a R2dbcDialect
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:
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
The preceding query can be parametrized and executed as follows:
List<Object[]> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann", 50});
db.execute("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:
db.execute("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", Arrays.asList(35, 50))
You can register a Statement
filter (StatementFilterFunction
) through DatabaseClient
to intercept and modify statements in their execution, as the following example shows:
db.execute("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …)
DatabaseClient
exposes also simplified filter(…)
overload accepting UnaryOperator<Statement>
:
db.execute("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(s -> s.returnGeneratedValues("id"))
.bind("name", …)
.bind("state", …)
db.execute("SELECT id, name, state FROM table")
.filter(s -> s.fetchSize(25))
StatementFilterFunction
allow filtering of the executed Statement
and filtering of Result
objects.