Skip to content

Commit b43b119

Browse files
committed
#23 - Add support for named parameters.
DatabaseClient now supports named parameters prefixed with a colon such as :name in addition to database-native bind markers. Named parameters thus are supported in annotated repository query methods which also increases portability of queries across database vendors. Named parameter support unrolls collection arguments to reduce the need for argument-specific SQL statements: db.execute() .sql("SELECT id, name, state FROM table WHERE age IN (:ages)") .bind("ages", Arrays.asList(35, 50)); Results in a query: SELECT id, name, state FROM table WHERE age IN (35, 50) Collection arguments containing nested object arrays can be used to use select lists: 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); translates to: SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) Original pull request: #47.
1 parent f90667b commit b43b119

24 files changed

+1490
-82
lines changed

src/main/asciidoc/new-features.adoc

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
[[new-features]]
22
= New & Noteworthy
33

4+
[[new-features.1-0-0-M2]]
5+
== What's New in Spring Data R2DBC 1.0.0 M2
6+
7+
* Support for named parameters.
8+
49
[[new-features.1-0-0-M1]]
510
== What's New in Spring Data R2DBC 1.0.0 M1
611

7-
* Initial R2DBC support through `DatabaseClient`
8-
* Initial Transaction support through `TransactionalDatabaseClient`
9-
* Initial R2DBC Repository Support through `R2dbcRepository`
10-
* Initial Dialect support for Postgres and Microsoft SQL Server
12+
* Initial R2DBC support through `DatabaseClient`.
13+
* Initial Transaction support through `TransactionalDatabaseClient`.
14+
* Initial R2DBC Repository Support through `R2dbcRepository`.
15+
* Initial Dialect support for Postgres and Microsoft SQL Server.

src/main/asciidoc/reference/r2dbc-repositories.adoc

+3-3
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Defining such a query is a matter of declaring a method on the repository interf
104104
----
105105
public interface PersonRepository extends ReactiveCrudRepository<Person, Long> {
106106
107-
@Query("SELECT * FROM person WHERE lastname = $1")
107+
@Query("SELECT * FROM person WHERE lastname = :lastname")
108108
Flux<Person> findByLastname(String lastname); <1>
109109
110110
@Query("SELECT firstname, lastname FROM person WHERE lastname = $1")
@@ -114,10 +114,10 @@ public interface PersonRepository extends ReactiveCrudRepository<Person, Long> {
114114
----
115115
<1> The `findByLastname` method shows a query for all people with the given last name.
116116
The query is provided as R2DBC repositories do not support query derivation.
117+
<2> A query for a single `Person` entity projecting only `firstname` and `lastname` columns.
117118
The annotated query uses native bind markers, which are Postgres bind markers in this example.
118-
<4> A query for a single `Person` entity projecting only `firstname` and `lastname` columns.
119119
====
120120

121121
NOTE: R2DBC repositories do not support query derivation.
122122

123-
NOTE: R2DBC repositories require native parameter bind markers that are bound by index.
123+
NOTE: R2DBC repositories bind parameters to placeholders by index.

src/main/asciidoc/reference/r2dbc.adoc

+55-14
Original file line numberDiff line numberDiff line change
@@ -446,20 +446,61 @@ Parameter binding supports various binding strategies:
446446
* By Index using zero-based parameter indexes.
447447
* By Name using the placeholder name.
448448

449-
The following example shows parameter binding for a PostgreSQL query:
449+
The following example shows parameter binding for a query:
450450

451451
[source,java]
452452
----
453453
db.execute()
454-
.sql("INSERT INTO person (id, name, age) VALUES($1, $2, $3)")
455-
.bind(0, "joe")
456-
.bind(1, "Joe")
457-
.bind(2, 34);
454+
.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
455+
.bind("id", "joe")
456+
.bind("name", "Joe")
457+
.bind("age", 34);
458458
----
459459

460-
NOTE: If you are familiar with JDBC, then you're also familiar with `?` (question mark) bind markers.
460+
.R2DBC Native Bind Markers
461+
****
462+
R2DBC uses database-native bind markers that depend on the actual database.
463+
If you are familiar with JDBC, then you're also familiar with `?` (question mark) bind markers.
461464
JDBC drivers translate question mark bind markers to database-native markers as part of statement execution.
462-
Make sure to use the appropriate bind markers that are supported by your database as R2DBC requires database-native parameter bind markers.
465+
466+
Postgres uses indexed markers (`$1`, `$2`), SQL Server uses named bind markers prefixed with `@` as its native bind marker syntax.
467+
Spring Data R2DBC leverages `Dialect` implementations 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.
468+
You can still use native bind markers if you prefer to do so.
469+
****
470+
471+
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.
472+
Nested object arrays are expanded to allow usage of e.g. select lists.
473+
474+
Consider the following query:
475+
476+
[source,sql]
477+
----
478+
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
479+
----
480+
481+
This query can be parametrized and executed as:
482+
483+
[source,java]
484+
----
485+
List<Object[]> tuples = new ArrayList<>();
486+
tuples.add(new Object[] {"John", 35});
487+
tuples.add(new Object[] {"Ann", 50});
488+
489+
db.execute()
490+
.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
491+
.bind("tuples", tuples);
492+
----
493+
494+
NOTE: Usage of select lists is vendor-dependent.
495+
496+
A simpler variant using `IN` predicates:
497+
498+
[source,java]
499+
----
500+
db.execute()
501+
.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
502+
.bind("ages", Arrays.asList(35, 50));
503+
----
463504

464505
[[r2dbc.datbaseclient.transactions]]
465506
=== Transactions
@@ -478,14 +519,14 @@ TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(
478519
479520
Flux<Void> completion = databaseClient.inTransaction(db -> {
480521
481-
return db.execute().sql("INSERT INTO person (id, name, age) VALUES($1, $2, $3)") //
482-
.bind(0, "joe") //
483-
.bind(1, "Joe") //
484-
.bind(2, 34) //
522+
return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
523+
.bind("id", "joe")
524+
.bind("name", "Joe")
525+
.bind("age", 34)
485526
.fetch().rowsUpdated()
486-
.then(db.execute().sql("INSERT INTO contacts (id, name) VALUES($1, $2)")
487-
.bind(0, "joe")
488-
.bind(1, "Joe")
527+
.then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)")
528+
.bind("id", "joe")
529+
.bind("name", "Joe")
489530
.fetch().rowsUpdated())
490531
.then();
491532
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.function;
17+
18+
import org.springframework.lang.Nullable;
19+
20+
/**
21+
* Interface that defines common functionality for objects that can offer parameter values for named bind parameters,
22+
* serving as argument for {@link NamedParameterExpander} operations.
23+
* <p>
24+
* This interface allows for the specification of the type in addition to parameter values. All parameter values and
25+
* types are identified by specifying the name of the parameter.
26+
* <p>
27+
* Intended to wrap various implementations like a {@link java.util.Map} with a consistent interface.
28+
*
29+
* @author Mark Paluch
30+
* @see MapBindParameterSource
31+
*/
32+
public interface BindParameterSource {
33+
34+
/**
35+
* Determine whether there is a value for the specified named parameter.
36+
*
37+
* @param paramName the name of the parameter.
38+
* @return {@literal true} if there is a value defined; {@literal false} otherwise.
39+
*/
40+
boolean hasValue(String paramName);
41+
42+
/**
43+
* Return the parameter value for the requested named parameter.
44+
*
45+
* @param paramName the name of the parameter.
46+
* @return the value of the specified parameter, can be {@literal null}.
47+
* @throws IllegalArgumentException if there is no value for the requested parameter.
48+
*/
49+
@Nullable
50+
Object getValue(String paramName) throws IllegalArgumentException;
51+
52+
/**
53+
* Determine the type for the specified named parameter.
54+
*
55+
* @param paramName the name of the parameter.
56+
* @return the type of the specified parameter, or {@link Object#getClass()} if not known.
57+
*/
58+
default Class<?> getType(String paramName) {
59+
return Object.class;
60+
}
61+
}

src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ interface Builder {
109109
*/
110110
Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy);
111111

112+
/**
113+
* Configures {@link NamedParameterExpander}.
114+
*
115+
* @param namedParameters must not be {@literal null}.
116+
* @return {@code this} {@link Builder}.
117+
* @see NamedParameterExpander#enabled()
118+
* @see NamedParameterExpander#disabled()
119+
*/
120+
Builder namedParameters(NamedParameterExpander namedParameters);
121+
112122
/**
113123
* Configures a {@link Consumer} to configure this builder.
114124
*
@@ -124,7 +134,12 @@ interface Builder {
124134
}
125135

126136
/**
127-
* Contract for specifying a SQL call along with options leading to the exchange.
137+
* Contract for specifying a SQL call along with options leading to the exchange. The SQL string can contain either
138+
* native parameter bind markers (e.g. {@literal $1, $2} for Postgres, {@literal @P0, @P1} for SQL Server) or named
139+
* parameters (e.g. {@literal :foo, :bar}) when {@link NamedParameterExpander} is enabled.
140+
*
141+
* @see NamedParameterExpander
142+
* @see DatabaseClient.Builder#namedParameters(NamedParameterExpander)
128143
*/
129144
interface SqlSpec {
130145

src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java

+20-6
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,6 @@
6666
*/
6767
class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
6868

69-
/**
70-
* Logger available to subclasses
71-
*/
7269
private final Log logger = LogFactory.getLog(getClass());
7370

7471
private final ConnectionFactory connector;
@@ -77,14 +74,18 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
7774

7875
private final ReactiveDataAccessStrategy dataAccessStrategy;
7976

77+
private final NamedParameterExpander namedParameters;
78+
8079
private final DefaultDatabaseClientBuilder builder;
8180

8281
DefaultDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
83-
ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) {
82+
ReactiveDataAccessStrategy dataAccessStrategy, NamedParameterExpander namedParameters,
83+
DefaultDatabaseClientBuilder builder) {
8484

8585
this.connector = connector;
8686
this.exceptionTranslator = exceptionTranslator;
8787
this.dataAccessStrategy = dataAccessStrategy;
88+
this.namedParameters = namedParameters;
8889
this.builder = builder;
8990
}
9091

@@ -325,8 +326,21 @@ <T> FetchSpec<T> exchange(String sql, BiFunction<Row, RowMetadata, T> mappingFun
325326
logger.debug("Executing SQL statement [" + sql + "]");
326327
}
327328

328-
Statement<?> statement = it.createStatement(sql);
329-
doBind(statement, byName, byIndex);
329+
BindableOperation operation = namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(),
330+
new MapBindParameterSource(byName));
331+
332+
Statement<?> statement = it.createStatement(operation.toQuery());
333+
334+
byName.forEach((name, o) -> {
335+
336+
if (o.getValue() != null) {
337+
operation.bind(statement, name, o.getValue());
338+
} else {
339+
operation.bindNull(statement, name, o.getType());
340+
}
341+
});
342+
343+
doBind(statement, Collections.emptyMap(), byIndex);
330344

331345
return statement;
332346
};

src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java

+50-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
3838
private @Nullable ConnectionFactory connectionFactory;
3939
private @Nullable R2dbcExceptionTranslator exceptionTranslator;
4040
private ReactiveDataAccessStrategy accessStrategy;
41+
private NamedParameterExpander namedParameters;
4142

4243
DefaultDatabaseClientBuilder() {}
4344

@@ -48,8 +49,13 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
4849
this.connectionFactory = other.connectionFactory;
4950
this.exceptionTranslator = other.exceptionTranslator;
5051
this.accessStrategy = other.accessStrategy;
52+
this.namedParameters = other.namedParameters;
5153
}
5254

55+
/*
56+
* (non-Javadoc)
57+
* @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#connectionFactory(io.r2dbc.spi.ConnectionFactory)
58+
*/
5359
@Override
5460
public Builder connectionFactory(ConnectionFactory factory) {
5561

@@ -59,6 +65,10 @@ public Builder connectionFactory(ConnectionFactory factory) {
5965
return this;
6066
}
6167

68+
/*
69+
* (non-Javadoc)
70+
* @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#exceptionTranslator(org.springframework.data.r2dbc.support.R2dbcExceptionTranslator)
71+
*/
6272
@Override
6373
public Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator) {
6474

@@ -68,6 +78,10 @@ public Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator)
6878
return this;
6979
}
7080

81+
/*
82+
* (non-Javadoc)
83+
* @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#dataAccessStrategy(org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy)
84+
*/
7185
@Override
7286
public Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy) {
7387

@@ -77,6 +91,23 @@ public Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy) {
7791
return this;
7892
}
7993

94+
/*
95+
* (non-Javadoc)
96+
* @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#namedParameters(org.springframework.data.r2dbc.function.NamedParameterExpander)
97+
*/
98+
@Override
99+
public Builder namedParameters(NamedParameterExpander namedParameters) {
100+
101+
Assert.notNull(namedParameters, "NamedParameterExpander must not be null!");
102+
103+
this.namedParameters = namedParameters;
104+
return this;
105+
}
106+
107+
/*
108+
* (non-Javadoc)
109+
* @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#build()
110+
*/
80111
@Override
81112
public DatabaseClient build() {
82113

@@ -97,19 +128,35 @@ public DatabaseClient build() {
97128
accessStrategy = new DefaultReactiveDataAccessStrategy(dialect);
98129
}
99130

100-
return doBuild(this.connectionFactory, exceptionTranslator, accessStrategy, new DefaultDatabaseClientBuilder(this));
131+
NamedParameterExpander namedParameters = this.namedParameters;
132+
133+
if (namedParameters == null) {
134+
namedParameters = NamedParameterExpander.enabled();
135+
}
136+
137+
return doBuild(this.connectionFactory, exceptionTranslator, accessStrategy, namedParameters,
138+
new DefaultDatabaseClientBuilder(this));
101139
}
102140

103141
protected DatabaseClient doBuild(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
104-
ReactiveDataAccessStrategy accessStrategy, DefaultDatabaseClientBuilder builder) {
105-
return new DefaultDatabaseClient(connector, exceptionTranslator, accessStrategy, builder);
142+
ReactiveDataAccessStrategy accessStrategy, NamedParameterExpander namedParameters,
143+
DefaultDatabaseClientBuilder builder) {
144+
return new DefaultDatabaseClient(connector, exceptionTranslator, accessStrategy, namedParameters, builder);
106145
}
107146

147+
/*
148+
* (non-Javadoc)
149+
* @see java.lang.Object#clone()
150+
*/
108151
@Override
109152
public DatabaseClient.Builder clone() {
110153
return new DefaultDatabaseClientBuilder(this);
111154
}
112155

156+
/*
157+
* (non-Javadoc)
158+
* @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#apply(java.util.function.Consumer)
159+
*/
113160
@Override
114161
public DatabaseClient.Builder apply(Consumer<DatabaseClient.Builder> builderConsumer) {
115162
Assert.notNull(builderConsumer, "BuilderConsumer must not be null");

0 commit comments

Comments
 (0)