#164 - Support @Query definitions with SpEL expressions.
We now support SpEL expressions in string-based queries to bind parameters for more dynamic queries. SpEL expressions are enclosed in :#{…} and rendered as synthetic named parameter so their values are substituted with bound parameters to avoid SQL injection attach vectors.
interface PersonRepository extends Repository<Person, String> {
@Query("SELECT * FROM person WHERE lastname = :#{'hello'}")
Mono<Person> findHello();
@Query("SELECT * FROM person WHERE lastname = :#{[0]} and firstname = :firstname")
Mono<Person> findByLastnameAndFirstname(@Param("value") String value, @Param("firstname") String firstname);
@Query("SELECT * FROM person WHERE lastname = :#{#person.name}")
Mono<Person> findByExample(@Param("person") Person person);
}
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
* `@Modifying` annotation for query methods to consume affected row count.
|
||||
* Repository `save(…)` with an associated Id terminates with `TransientDataAccessException` if the row does not exist in the database.
|
||||
* Added `SingleConnectionConnectionFactory` for testing using connection singletons.
|
||||
* Support for {spring-framework-ref}/core.html#expressions[SpEL expressions] in `@Query`.
|
||||
|
||||
[[new-features.1-0-0-RC1]]
|
||||
== What's New in Spring Data R2DBC 1.0.0 RC1
|
||||
|
||||
@@ -149,6 +149,35 @@ The result of a modifying query can be:
|
||||
The `@Modifying` annotation is only relevant in combination with the `@Query` annotation.
|
||||
Derived custom methods do not require this annotation.
|
||||
|
||||
[[r2dbc.repositories.queries.spel]]
|
||||
=== Queries with SpEL Expressions
|
||||
|
||||
Query string definitions can be used together with SpEL expressions to create dynamic queries at runtime.
|
||||
SpEL expressions can provide predicate values which are evaluated right before executing the query.
|
||||
|
||||
Expressions expose method arguments through an array that contains all the arguments.
|
||||
The following query uses `[0]`
|
||||
to declare the predicate value for `lastname` (which is equivalent to the `:lastname` parameter binding):
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public interface PersonRepository extends ReactiveCrudRepository<Person, String> {
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = :#{[0]} }")
|
||||
List<Person> findByQueryWithExpression(String lastname);
|
||||
}
|
||||
----
|
||||
|
||||
SpEL in query strings can be a powerful way to enhance queries.
|
||||
However, they can also accept a broad range of unwanted arguments.
|
||||
You should make sure to sanitize strings before passing them to the query to avoid unwanted changes to your query.
|
||||
|
||||
Expression support is extensible through the Query SPI: `org.springframework.data.spel.spi.EvaluationContextExtension`.
|
||||
The Query SPI can contribute properties and functions and can customize the root object.
|
||||
Extensions are retrieved from the application context at the time of SpEL evaluation when the query is built.
|
||||
|
||||
TIP: When using SpEL expressions in combination with plain parameters, use named parameter notation instead of native bind markers to ensure a proper binding order.
|
||||
|
||||
[[r2dbc.entity-persistence.state-detection-strategies]]
|
||||
=== Entity State Detection Strategies
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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.repository.query;
|
||||
|
||||
import static org.springframework.data.r2dbc.repository.query.ExpressionQuery.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.data.r2dbc.core.DatabaseClient;
|
||||
import org.springframework.data.r2dbc.mapping.SettableValue;
|
||||
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a
|
||||
* {@link String}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 1.1
|
||||
*/
|
||||
class ExpressionEvaluatingParameterBinder {
|
||||
|
||||
private final SpelExpressionParser expressionParser;
|
||||
|
||||
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
|
||||
|
||||
private final ExpressionQuery expressionQuery;
|
||||
|
||||
private final Map<String, Boolean> namedParameters = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Creates new {@link ExpressionEvaluatingParameterBinder}
|
||||
*
|
||||
* @param expressionParser must not be {@literal null}.
|
||||
* @param evaluationContextProvider must not be {@literal null}.
|
||||
* @param expressionQuery must not be {@literal null}.
|
||||
*/
|
||||
ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider, ExpressionQuery expressionQuery) {
|
||||
|
||||
Assert.notNull(expressionParser, "ExpressionParser must not be null");
|
||||
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null");
|
||||
Assert.notNull(expressionQuery, "ExpressionQuery must not be null");
|
||||
|
||||
this.expressionParser = expressionParser;
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
this.expressionQuery = expressionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind values provided by {@link RelationalParameterAccessor} to placeholders in {@link ExpressionQuery} while
|
||||
* considering potential conversions and parameter types.
|
||||
*
|
||||
* @param bindSpec must not be {@literal null}.
|
||||
* @param parameterAccessor must not be {@literal null}.
|
||||
*/
|
||||
public <T extends DatabaseClient.BindSpec<T>> T bind(T bindSpec, RelationalParameterAccessor parameterAccessor) {
|
||||
|
||||
Object[] values = parameterAccessor.getValues();
|
||||
Parameters<?, ?> bindableParameters = parameterAccessor.getBindableParameters();
|
||||
|
||||
T bindSpecToUse = bindExpressions(bindSpec, values, bindableParameters);
|
||||
bindSpecToUse = bindParameters(bindSpecToUse, parameterAccessor.hasBindableNullValue(), values, bindableParameters);
|
||||
|
||||
return bindSpecToUse;
|
||||
}
|
||||
|
||||
private <T extends DatabaseClient.BindSpec<T>> T bindExpressions(T bindSpec, Object[] values,
|
||||
Parameters<?, ?> bindableParameters) {
|
||||
|
||||
T bindSpecToUse = bindSpec;
|
||||
|
||||
for (ParameterBinding binding : expressionQuery.getBindings()) {
|
||||
|
||||
SettableValue valueForBinding = getParameterValueForBinding(bindableParameters, values, binding);
|
||||
|
||||
if (valueForBinding.isEmpty()) {
|
||||
bindSpecToUse = bindSpecToUse.bindNull(binding.getParameterName(), valueForBinding.getType());
|
||||
} else {
|
||||
bindSpecToUse = bindSpecToUse.bind(binding.getParameterName(), valueForBinding.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return bindSpecToUse;
|
||||
}
|
||||
|
||||
private <T extends DatabaseClient.BindSpec<T>> T bindParameters(T bindSpec, boolean bindableNull, Object[] values,
|
||||
Parameters<?, ?> bindableParameters) {
|
||||
|
||||
T bindSpecToUse = bindSpec;
|
||||
int index = 0;
|
||||
int bindingIndex = 0;
|
||||
|
||||
for (Object value : values) {
|
||||
|
||||
Parameter bindableParameter = bindableParameters.getBindableParameter(index++);
|
||||
|
||||
Optional<String> name = bindableParameter.getName();
|
||||
|
||||
if ((name.isPresent() && isNamedParameterUsed(name)) || !expressionQuery.getBindings().isEmpty()) {
|
||||
|
||||
if (isNamedParameterUsed(name)) {
|
||||
|
||||
if (value == null) {
|
||||
if (bindableNull) {
|
||||
bindSpecToUse = bindSpecToUse.bindNull(name.get(), bindableParameter.getType());
|
||||
}
|
||||
} else {
|
||||
bindSpecToUse = bindSpecToUse.bind(name.get(), value);
|
||||
}
|
||||
}
|
||||
|
||||
// skip unused named parameters if there is SpEL
|
||||
} else {
|
||||
if (value == null) {
|
||||
if (bindableNull) {
|
||||
bindSpecToUse = bindSpecToUse.bindNull(bindingIndex++, bindableParameter.getType());
|
||||
}
|
||||
} else {
|
||||
bindSpecToUse = bindSpecToUse.bind(bindingIndex++, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bindSpecToUse;
|
||||
}
|
||||
|
||||
private boolean isNamedParameterUsed(Optional<String> name) {
|
||||
|
||||
if (!name.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return namedParameters.computeIfAbsent(name.get(), it -> {
|
||||
|
||||
Pattern namedParameterPattern = Pattern.compile("(\\W)[:#$@]" + Pattern.quote(it) + "(\\W|$)");
|
||||
return namedParameterPattern.matcher(expressionQuery.getQuery()).find();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to be used for the given {@link ParameterBinding}.
|
||||
*
|
||||
* @param parameters must not be {@literal null}.
|
||||
* @param binding must not be {@literal null}.
|
||||
* @return the value used for the given {@link ParameterBinding}.
|
||||
*/
|
||||
private SettableValue getParameterValueForBinding(Parameters<?, ?> parameters, Object[] values,
|
||||
ParameterBinding binding) {
|
||||
return evaluateExpression(binding.getExpression(), parameters, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given {@code expressionString}.
|
||||
*
|
||||
* @param expressionString must not be {@literal null} or empty.
|
||||
* @param parameters must not be {@literal null}.
|
||||
* @param parameterValues must not be {@literal null}.
|
||||
* @return the value of the {@code expressionString} evaluation.
|
||||
*/
|
||||
private SettableValue evaluateExpression(String expressionString, Parameters<?, ?> parameters,
|
||||
Object[] parameterValues) {
|
||||
|
||||
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues);
|
||||
Expression expression = expressionParser.parseExpression(expressionString);
|
||||
|
||||
Object value = expression.getValue(evaluationContext, Object.class);
|
||||
Class<?> valueType = expression.getValueType(evaluationContext);
|
||||
|
||||
return SettableValue.fromOrEmpty(value, valueType != null ? valueType : Object.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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.repository.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Query using Spring Expression Language to indicate parameter bindings. Queries using SpEL use {@code :#{…}} to
|
||||
* enclose expressions. Expressions are substituted with synthetic named parameters and require therefore enabled named
|
||||
* parameter expansion for execution.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 1.1
|
||||
*/
|
||||
class ExpressionQuery {
|
||||
|
||||
private static final char CURLY_BRACE_OPEN = '{';
|
||||
private static final char CURLY_BRACE_CLOSE = '}';
|
||||
|
||||
private static final String SYNTHETIC_PARAMETER_TEMPLATE = "__synthetic_%d__";
|
||||
|
||||
private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[:]#\\{(.*)}");
|
||||
|
||||
private final String query;
|
||||
|
||||
private final List<ParameterBinding> parameterBindings;
|
||||
|
||||
private ExpressionQuery(String query, List<ParameterBinding> parameterBindings) {
|
||||
this.query = query;
|
||||
this.parameterBindings = parameterBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link ExpressionQuery} from a {@code query}.
|
||||
*
|
||||
* @param query the query string to parse.
|
||||
* @return the parsed {@link ExpressionQuery}.
|
||||
*/
|
||||
public static ExpressionQuery create(String query) {
|
||||
|
||||
List<ParameterBinding> parameterBindings = new ArrayList<>();
|
||||
String rewritten = transformQueryAndCollectExpressionParametersIntoBindings(query, parameterBindings);
|
||||
|
||||
return new ExpressionQuery(rewritten, parameterBindings);
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public List<ParameterBinding> getBindings() {
|
||||
return parameterBindings;
|
||||
}
|
||||
|
||||
private static String transformQueryAndCollectExpressionParametersIntoBindings(String input,
|
||||
List<ParameterBinding> bindings) {
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
int startIndex = 0;
|
||||
int currentPosition = 0;
|
||||
int parameterIndex = 0;
|
||||
|
||||
while (currentPosition < input.length()) {
|
||||
|
||||
Matcher matcher = findNextBindingOrExpression(input, currentPosition);
|
||||
|
||||
// no expression parameter found
|
||||
if (matcher == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
int exprStart = matcher.start();
|
||||
currentPosition = exprStart;
|
||||
|
||||
// eat parameter expression
|
||||
int curlyBraceOpenCount = 1;
|
||||
currentPosition += 3;
|
||||
|
||||
while (curlyBraceOpenCount > 0 && currentPosition < input.length()) {
|
||||
switch (input.charAt(currentPosition++)) {
|
||||
case CURLY_BRACE_OPEN:
|
||||
curlyBraceOpenCount++;
|
||||
break;
|
||||
case CURLY_BRACE_CLOSE:
|
||||
curlyBraceOpenCount--;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
|
||||
String parameterName = String.format(SYNTHETIC_PARAMETER_TEMPLATE, parameterIndex++);
|
||||
result.append(':').append(parameterName);
|
||||
|
||||
bindings.add(ParameterBinding.named(parameterName, matcher.group(1)));
|
||||
|
||||
currentPosition = matcher.end();
|
||||
startIndex = currentPosition;
|
||||
}
|
||||
|
||||
return result.append(input.subSequence(currentPosition, input.length())).toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Matcher findNextBindingOrExpression(String input, int position) {
|
||||
|
||||
Matcher matcher = EXPRESSION_BINDING_PATTERN.matcher(input);
|
||||
if (matcher.find(position)) {
|
||||
return matcher;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* A SpEL parameter binding.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
static class ParameterBinding {
|
||||
|
||||
private final String parameterName;
|
||||
private final String expression;
|
||||
|
||||
private ParameterBinding(String parameterName, String expression) {
|
||||
|
||||
this.expression = expression;
|
||||
this.parameterName = parameterName;
|
||||
}
|
||||
|
||||
static ParameterBinding named(String name, String expression) {
|
||||
return new ParameterBinding(name, expression);
|
||||
}
|
||||
|
||||
String getExpression() {
|
||||
return expression;
|
||||
}
|
||||
|
||||
String getParameterName() {
|
||||
return parameterName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,11 @@
|
||||
*/
|
||||
package org.springframework.data.r2dbc.repository.query;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.data.r2dbc.convert.R2dbcConverter;
|
||||
import org.springframework.data.r2dbc.core.DatabaseClient;
|
||||
import org.springframework.data.r2dbc.core.DatabaseClient.BindSpec;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -34,14 +27,15 @@ import org.springframework.util.Assert;
|
||||
/**
|
||||
* String-based {@link StringBasedR2dbcQuery} implementation.
|
||||
* <p>
|
||||
* A {@link StringBasedR2dbcQuery} expects a query method to be annotated with {@link Query} with a SQL query.
|
||||
* A {@link StringBasedR2dbcQuery} expects a query method to be annotated with {@link Query} with a SQL query. Supports
|
||||
* named parameters (if enabled on {@link DatabaseClient}) and SpEL expressions enclosed with {@code :#{…}}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
|
||||
|
||||
private final String sql;
|
||||
private final Map<String, Boolean> namedParameters = new ConcurrentHashMap<>();
|
||||
private final ExpressionQuery expressionQuery;
|
||||
private final ExpressionEvaluatingParameterBinder binder;
|
||||
|
||||
/**
|
||||
* Creates a new {@link StringBasedR2dbcQuery} for the given {@link StringBasedR2dbcQuery}, {@link DatabaseClient},
|
||||
@@ -78,7 +72,8 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
|
||||
|
||||
Assert.hasText(query, "Query must not be empty");
|
||||
|
||||
this.sql = query;
|
||||
this.expressionQuery = ExpressionQuery.create(query);
|
||||
this.binder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider, expressionQuery);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -91,58 +86,14 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
|
||||
|
||||
@Override
|
||||
public <T extends BindSpec<T>> T bind(T bindSpec) {
|
||||
|
||||
T bindSpecToUse = bindSpec;
|
||||
|
||||
Parameters<?, ?> bindableParameters = accessor.getBindableParameters();
|
||||
|
||||
int index = 0;
|
||||
int bindingIndex = 0;
|
||||
for (Object value : accessor.getValues()) {
|
||||
|
||||
Parameter bindableParameter = bindableParameters.getBindableParameter(index++);
|
||||
|
||||
Optional<String> name = bindableParameter.getName();
|
||||
if (isNamedParameter(name)) {
|
||||
if (value == null) {
|
||||
if (accessor.hasBindableNullValue()) {
|
||||
bindSpecToUse = bindSpecToUse.bindNull(name.get(), bindableParameter.getType());
|
||||
}
|
||||
} else {
|
||||
bindSpecToUse = bindSpecToUse.bind(name.get(), value);
|
||||
}
|
||||
} else {
|
||||
if (value == null) {
|
||||
if (accessor.hasBindableNullValue()) {
|
||||
bindSpecToUse = bindSpecToUse.bindNull(bindingIndex++, bindableParameter.getType());
|
||||
}
|
||||
} else {
|
||||
bindSpecToUse = bindSpecToUse.bind(bindingIndex++, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bindSpecToUse;
|
||||
}
|
||||
|
||||
private boolean isNamedParameter(Optional<String> name) {
|
||||
|
||||
if (!name.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return namedParameters.computeIfAbsent(name.get(), it -> {
|
||||
|
||||
Pattern namedParameterPattern = Pattern.compile("(\\W)" + Pattern.quote(it) + "(\\W|$)");
|
||||
return namedParameterPattern.matcher(this.get()).find();
|
||||
});
|
||||
|
||||
return binder.bind(bindSpec, accessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
return sql;
|
||||
return expressionQuery.getQuery();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -87,6 +87,20 @@ public class StringBasedR2dbcQueryUnitTests {
|
||||
verify(bindSpec).bind(0, "White");
|
||||
}
|
||||
|
||||
@Test // gh-164
|
||||
public void bindsPositionalPropertyCorrectly() {
|
||||
|
||||
StringBasedR2dbcQuery query = getQueryMethod("findByLastnamePositional", String.class);
|
||||
R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod(), "White");
|
||||
|
||||
BindableQuery stringQuery = query.createQuery(accessor);
|
||||
|
||||
assertThat(stringQuery.get()).isEqualTo("SELECT * FROM person WHERE lastname = $1");
|
||||
assertThat(stringQuery.bind(bindSpec)).isNotNull();
|
||||
|
||||
verify(bindSpec).bind(0, "White");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindsByNamedParameter() {
|
||||
|
||||
@@ -129,6 +143,84 @@ public class StringBasedR2dbcQueryUnitTests {
|
||||
verify(bindSpec).bind(0, "White");
|
||||
}
|
||||
|
||||
@Test // gh-164
|
||||
public void bindsSimpleSpelQuery() {
|
||||
|
||||
StringBasedR2dbcQuery query = getQueryMethod("simpleSpel");
|
||||
R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod());
|
||||
|
||||
BindableQuery stringQuery = query.createQuery(accessor);
|
||||
|
||||
assertThat(stringQuery.get()).isEqualTo("SELECT * FROM person WHERE lastname = :__synthetic_0__");
|
||||
assertThat(stringQuery.bind(bindSpec)).isNotNull();
|
||||
|
||||
verify(bindSpec).bind("__synthetic_0__", "hello");
|
||||
}
|
||||
|
||||
@Test // gh-164
|
||||
public void bindsIndexedSpelQuery() {
|
||||
|
||||
StringBasedR2dbcQuery query = getQueryMethod("simpleIndexedSpel", String.class);
|
||||
R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod(), "White");
|
||||
|
||||
BindableQuery stringQuery = query.createQuery(accessor);
|
||||
|
||||
assertThat(stringQuery.get()).isEqualTo("SELECT * FROM person WHERE lastname = :__synthetic_0__");
|
||||
assertThat(stringQuery.bind(bindSpec)).isNotNull();
|
||||
|
||||
verify(bindSpec).bind("__synthetic_0__", "White");
|
||||
verifyNoMoreInteractions(bindSpec);
|
||||
}
|
||||
|
||||
@Test // gh-164
|
||||
public void bindsPositionalSpelQuery() {
|
||||
|
||||
StringBasedR2dbcQuery query = getQueryMethod("simplePositionalSpel", String.class, String.class);
|
||||
R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod(), "White", "Walter");
|
||||
|
||||
BindableQuery stringQuery = query.createQuery(accessor);
|
||||
|
||||
assertThat(stringQuery.get())
|
||||
.isEqualTo("SELECT * FROM person WHERE lastname = :__synthetic_0__ and firstname = :firstname");
|
||||
assertThat(stringQuery.bind(bindSpec)).isNotNull();
|
||||
|
||||
verify(bindSpec).bind("__synthetic_0__", "White");
|
||||
verify(bindSpec).bind("firstname", "Walter");
|
||||
verifyNoMoreInteractions(bindSpec);
|
||||
}
|
||||
|
||||
@Test // gh-164
|
||||
public void bindsPositionalNamedSpelQuery() {
|
||||
|
||||
StringBasedR2dbcQuery query = getQueryMethod("simpleNamedSpel", String.class, String.class);
|
||||
R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod(), "White", "Walter");
|
||||
|
||||
BindableQuery stringQuery = query.createQuery(accessor);
|
||||
|
||||
assertThat(stringQuery.get())
|
||||
.isEqualTo("SELECT * FROM person WHERE lastname = :__synthetic_0__ and firstname = :firstname");
|
||||
assertThat(stringQuery.bind(bindSpec)).isNotNull();
|
||||
|
||||
verify(bindSpec).bind("__synthetic_0__", "White");
|
||||
verify(bindSpec).bind("firstname", "Walter");
|
||||
verifyNoMoreInteractions(bindSpec);
|
||||
}
|
||||
|
||||
@Test // gh-164
|
||||
public void bindsComplexSpelQuery() {
|
||||
|
||||
StringBasedR2dbcQuery query = getQueryMethod("queryWithSpelObject", Person.class);
|
||||
R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod(), new Person("Walter"));
|
||||
|
||||
BindableQuery stringQuery = query.createQuery(accessor);
|
||||
|
||||
assertThat(stringQuery.get()).isEqualTo("SELECT * FROM person WHERE lastname = :__synthetic_0__");
|
||||
assertThat(stringQuery.bind(bindSpec)).isNotNull();
|
||||
|
||||
verify(bindSpec).bind("__synthetic_0__", "Walter");
|
||||
verifyNoMoreInteractions(bindSpec);
|
||||
}
|
||||
|
||||
private StringBasedR2dbcQuery getQueryMethod(String name, Class<?>... args) {
|
||||
|
||||
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
|
||||
@@ -143,7 +235,10 @@ public class StringBasedR2dbcQueryUnitTests {
|
||||
private interface SampleRepository extends Repository<Person, String> {
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = $1")
|
||||
Person findByLastname(String lastname);
|
||||
Person findByLastname(@Param("lastname") String lastname);
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = $1")
|
||||
Person findByLastnamePositional(String lastname);
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = :lastname")
|
||||
Person findByNamedParameter(@Param("lastname") String lastname);
|
||||
@@ -153,9 +248,33 @@ public class StringBasedR2dbcQueryUnitTests {
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = @lastname")
|
||||
Person findByNamedBindMarker(@Param("lastname") String lastname);
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = :#{'hello'}")
|
||||
Person simpleSpel();
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = :#{[0]}")
|
||||
Person simpleIndexedSpel(String value);
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = :#{[0]} and firstname = :firstname")
|
||||
Person simpleNamedSpel(@Param("value") String value, @Param("firstname") String firstname);
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = :#{#value} and firstname = :firstname")
|
||||
Person simplePositionalSpel(@Param("value") String value, @Param("firstname") String firstname);
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = :#{#person.name}")
|
||||
Person queryWithSpelObject(@Param("person") Person person);
|
||||
}
|
||||
|
||||
static class Person {
|
||||
|
||||
String name;
|
||||
|
||||
public Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user