Skip to content

Add support for named parameters #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-r2dbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<version>1.0.0.gh-23-SNAPSHOT</version>

<name>Spring Data R2DBC</name>
<description>Spring Data module for R2DBC.</description>
Expand Down
13 changes: 9 additions & 4 deletions src/main/asciidoc/new-features.adoc
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
[[new-features]]
= New & Noteworthy

[[new-features.1-0-0-M2]]
== What's New in Spring Data R2DBC 1.0.0 M2

* Support for named parameters.

[[new-features.1-0-0-M1]]
== What's New in Spring Data R2DBC 1.0.0 M1

* Initial R2DBC support through `DatabaseClient`
* Initial Transaction support through `TransactionalDatabaseClient`
* Initial R2DBC Repository Support through `R2dbcRepository`
* Initial Dialect support for Postgres and Microsoft SQL Server
* Initial R2DBC support through `DatabaseClient`.
* Initial Transaction support through `TransactionalDatabaseClient`.
* Initial R2DBC Repository Support through `R2dbcRepository`.
* Initial Dialect support for Postgres and Microsoft SQL Server.
6 changes: 3 additions & 3 deletions src/main/asciidoc/reference/r2dbc-repositories.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Defining such a query is a matter of declaring a method on the repository interf
----
public interface PersonRepository extends ReactiveCrudRepository<Person, Long> {

@Query("SELECT * FROM person WHERE lastname = $1")
@Query("SELECT * FROM person WHERE lastname = :lastname")
Flux<Person> findByLastname(String lastname); <1>

@Query("SELECT firstname, lastname FROM person WHERE lastname = $1")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the $1 syntax left in there an oversight or an attempt to demonstrate database dependent options?

If the latter I think we need an explanatory note to that extent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intentionally there, to occasionally use native bind markers. See the callout description:

The annotated query uses native bind markers, which are Postgres bind markers in this example.

Expand All @@ -114,10 +114,10 @@ public interface PersonRepository extends ReactiveCrudRepository<Person, Long> {
----
<1> The `findByLastname` method shows a query for all people with the given last name.
The query is provided as R2DBC repositories do not support query derivation.
<2> A query for a single `Person` entity projecting only `firstname` and `lastname` columns.
The annotated query uses native bind markers, which are Postgres bind markers in this example.
<4> A query for a single `Person` entity projecting only `firstname` and `lastname` columns.
====

NOTE: R2DBC repositories do not support query derivation.

NOTE: R2DBC repositories require native parameter bind markers that are bound by index.
NOTE: R2DBC repositories bind internally parameters to placeholders via `Statement.bind(…)` by index.
74 changes: 59 additions & 15 deletions src/main/asciidoc/reference/r2dbc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -446,20 +446,64 @@ 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 PostgreSQL query:
The following example shows parameter binding for a query:

[source,java]
----
db.execute()
.sql("INSERT INTO person (id, name, age) VALUES($1, $2, $3)")
.bind(0, "joe")
.bind(1, "Joe")
.bind(2, 34);
.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
----

NOTE: If you are familiar with JDBC, then you're also familiar with `?` (question mark) bind markers.
JDBC drivers translate question mark bind markers to database-native markers as part of statement execution.
Make sure to use the appropriate bind markers that are supported by your database as R2DBC requires database-native parameter bind markers.
.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
Expand All @@ -478,14 +522,14 @@ TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(

Flux<Void> completion = databaseClient.inTransaction(db -> {

return db.execute().sql("INSERT INTO person (id, name, age) VALUES($1, $2, $3)") //
.bind(0, "joe") //
.bind(1, "Joe") //
.bind(2, 34) //
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($1, $2)")
.bind(0, "joe")
.bind(1, "Joe")
.then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)")
.bind("id", "joe")
.bind("name", "Joe")
.fetch().rowsUpdated())
.then();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.function;

import org.springframework.lang.Nullable;

/**
* Interface that defines common functionality for objects that can offer parameter values for named bind parameters,
* serving as argument for {@link NamedParameterExpander} operations.
* <p>
* This interface allows for the specification of the type in addition to parameter values. All parameter values and
* types are identified by specifying the name of the parameter.
* <p>
* Intended to wrap various implementations like a {@link java.util.Map} with a consistent interface.
*
* @author Mark Paluch
* @see MapBindParameterSource
*/
public interface BindParameterSource {

/**
* Determine whether there is a value for the specified named parameter.
*
* @param paramName the name of the parameter.
* @return {@literal true} if there is a value defined; {@literal false} otherwise.
*/
boolean hasValue(String paramName);

/**
* Return the parameter value for the requested named parameter.
*
* @param paramName the name of the parameter.
* @return the value of the specified parameter, can be {@literal null}.
* @throws IllegalArgumentException if there is no value for the requested parameter.
*/
@Nullable
Object getValue(String paramName) throws IllegalArgumentException;

/**
* Determine the type for the specified named parameter.
*
* @param paramName the name of the parameter.
* @return the type of the specified parameter, or {@link Object#getClass()} if not known.
*/
default Class<?> getType(String paramName) {
return Object.class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ interface Builder {
*/
Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy);

/**
* Configures {@link NamedParameterExpander}.
*
* @param expander must not be {@literal null}.
* @return {@code this} {@link Builder}.
* @see NamedParameterExpander#enabled()
* @see NamedParameterExpander#disabled()
*/
Builder namedParameters(NamedParameterExpander expander);

/**
* Configures a {@link Consumer} to configure this builder.
*
Expand All @@ -124,7 +134,12 @@ interface Builder {
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,6 @@
*/
class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {

/**
* Logger available to subclasses
*/
private final Log logger = LogFactory.getLog(getClass());

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

private final ReactiveDataAccessStrategy dataAccessStrategy;

private final NamedParameterExpander namedParameters;

private final DefaultDatabaseClientBuilder builder;

DefaultDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) {
ReactiveDataAccessStrategy dataAccessStrategy, NamedParameterExpander namedParameters,
DefaultDatabaseClientBuilder builder) {

this.connector = connector;
this.exceptionTranslator = exceptionTranslator;
this.dataAccessStrategy = dataAccessStrategy;
this.namedParameters = namedParameters;
this.builder = builder;
}

Expand Down Expand Up @@ -253,21 +254,30 @@ protected DefaultGenericExecuteSpec createGenericExecuteSpec(Supplier<String> sq
private static void doBind(Statement<?> statement, Map<String, SettableValue> byName,
Map<Integer, SettableValue> byIndex) {

byIndex.forEach((i, o) -> {
bindByIndex(statement, byIndex);
bindByName(statement, byName);
}

private static void bindByName(Statement<?> statement, Map<String, SettableValue> byName) {

byName.forEach((name, o) -> {

if (o.getValue() != null) {
statement.bind(i.intValue(), o.getValue());
statement.bind(name, o.getValue());
} else {
statement.bindNull(i.intValue(), o.getType());
statement.bindNull(name, o.getType());
}
});
}

byName.forEach((name, o) -> {
private static void bindByIndex(Statement<?> statement, Map<Integer, SettableValue> byIndex) {

byIndex.forEach((i, o) -> {

if (o.getValue() != null) {
statement.bind(name, o.getValue());
statement.bind(i.intValue(), o.getValue());
} else {
statement.bindNull(name, o.getType());
statement.bindNull(i.intValue(), o.getType());
}
});
}
Expand Down Expand Up @@ -325,8 +335,21 @@ <T> FetchSpec<T> exchange(String sql, BiFunction<Row, RowMetadata, T> mappingFun
logger.debug("Executing SQL statement [" + sql + "]");
}

Statement<?> statement = it.createStatement(sql);
doBind(statement, byName, byIndex);
BindableOperation operation = namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(),
new MapBindParameterSource(byName));

Statement<?> statement = it.createStatement(operation.toQuery());

byName.forEach((name, o) -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doing the binding by name inline but calling a method for binding by index looks unpleasant unsymmetrically.


if (o.getValue() != null) {
operation.bind(statement, name, o.getValue());
} else {
operation.bindNull(statement, name, o.getType());
}
});

bindByIndex(statement, byIndex);

return statement;
};
Expand Down
Loading