Skip to content

Commit 2c2f4c0

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))
1 parent 2193e79 commit 2c2f4c0

24 files changed

+1458
-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 `$1`, `$2` and so on, SQL Server uses named bind markers prefixed with `@` as their native bind markers.
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 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 NamedParameterSupport} 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 whether there is a value defined.
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.
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 NamedParameterSupport}.
114+
*
115+
* @param namedParameterSupport must not be {@literal null}.
116+
* @return {@code this} {@link Builder}.
117+
* @see NamedParameterSupport#enabled()
118+
* @see NamedParameterSupport#disabled()
119+
*/
120+
Builder namedParameters(NamedParameterSupport namedParameterSupport);
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 NamedParameterSupport} is enabled.
140+
*
141+
* @see NamedParameterSupport
142+
* @see DatabaseClient.Builder#namedParameters(NamedParameterSupport)
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 NamedParameterSupport namedParameterSupport;
78+
8079
private final DefaultDatabaseClientBuilder builder;
8180

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

8585
this.connector = connector;
8686
this.exceptionTranslator = exceptionTranslator;
8787
this.dataAccessStrategy = dataAccessStrategy;
88+
this.namedParameterSupport = namedParameterSupport;
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 = namedParameterSupport.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

+22-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 NamedParameterSupport namedParameterSupport;
4142

4243
DefaultDatabaseClientBuilder() {}
4344

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

5355
@Override
@@ -77,6 +79,15 @@ public Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy) {
7779
return this;
7880
}
7981

82+
@Override
83+
public Builder namedParameters(NamedParameterSupport namedParameterSupport) {
84+
85+
Assert.notNull(namedParameterSupport, "NamedParameterSupportAccessStrategy must not be null!");
86+
87+
this.namedParameterSupport = namedParameterSupport;
88+
return this;
89+
}
90+
8091
@Override
8192
public DatabaseClient build() {
8293

@@ -97,12 +108,20 @@ public DatabaseClient build() {
97108
accessStrategy = new DefaultReactiveDataAccessStrategy(dialect);
98109
}
99110

100-
return doBuild(this.connectionFactory, exceptionTranslator, accessStrategy, new DefaultDatabaseClientBuilder(this));
111+
NamedParameterSupport namedParameterSupport = this.namedParameterSupport;
112+
113+
if (namedParameterSupport == null) {
114+
namedParameterSupport = NamedParameterSupport.enabled();
115+
}
116+
117+
return doBuild(this.connectionFactory, exceptionTranslator, accessStrategy, namedParameterSupport,
118+
new DefaultDatabaseClientBuilder(this));
101119
}
102120

103121
protected DatabaseClient doBuild(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
104-
ReactiveDataAccessStrategy accessStrategy, DefaultDatabaseClientBuilder builder) {
105-
return new DefaultDatabaseClient(connector, exceptionTranslator, accessStrategy, builder);
122+
ReactiveDataAccessStrategy accessStrategy, NamedParameterSupport namedParameterSupport,
123+
DefaultDatabaseClientBuilder builder) {
124+
return new DefaultDatabaseClient(connector, exceptionTranslator, accessStrategy, namedParameterSupport, builder);
106125
}
107126

108127
@Override

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

+10
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.data.r2dbc.dialect.ArrayColumns;
4242
import org.springframework.data.r2dbc.dialect.BindMarker;
4343
import org.springframework.data.r2dbc.dialect.BindMarkers;
44+
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
4445
import org.springframework.data.r2dbc.dialect.Dialect;
4546
import org.springframework.data.r2dbc.dialect.LimitClause;
4647
import org.springframework.data.r2dbc.dialect.LimitClause.Position;
@@ -238,6 +239,15 @@ public String getTableName(Class<?> type) {
238239
return getRequiredPersistentEntity(type).getTableName();
239240
}
240241

242+
/*
243+
* (non-Javadoc)
244+
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getBindMarkersFactory()
245+
*/
246+
@Override
247+
public BindMarkersFactory getBindMarkersFactory() {
248+
return dialect.getBindMarkersFactory();
249+
}
250+
241251
private RelationalPersistentEntity<?> getRequiredPersistentEntity(Class<?> typeToRead) {
242252
return mappingContext.getRequiredPersistentEntity(typeToRead);
243253
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
class DefaultTransactionalDatabaseClient extends DefaultDatabaseClient implements TransactionalDatabaseClient {
4040

4141
DefaultTransactionalDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
42-
ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) {
43-
super(connector, exceptionTranslator, dataAccessStrategy, builder);
42+
ReactiveDataAccessStrategy dataAccessStrategy, NamedParameterSupport namedParameterSupport,
43+
DefaultDatabaseClientBuilder builder) {
44+
super(connector, exceptionTranslator, dataAccessStrategy, namedParameterSupport, builder);
4445
}
4546

4647
@Override

0 commit comments

Comments
 (0)