From 6027e29305fa409b1352e2de88a5ab06991e3dfd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 20 Feb 2020 10:36:18 +0100 Subject: [PATCH] #164 - Support @Query definitions with SpEL expressions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 { @Query("SELECT * FROM person WHERE lastname = :#{'hello'}") Mono findHello(); @Query("SELECT * FROM person WHERE lastname = :#{[0]} and firstname = :firstname") Mono findByLastnameAndFirstname(@Param("value") String value, @Param("firstname") String firstname); @Query("SELECT * FROM person WHERE lastname = :#{#person.name}") Mono findByExample(@Param("person") Person person); } --- src/main/asciidoc/new-features.adoc | 1 + .../reference/r2dbc-repositories.adoc | 29 +++ .../ExpressionEvaluatingParameterBinder.java | 194 ++++++++++++++++++ .../repository/query/ExpressionQuery.java | 168 +++++++++++++++ .../query/StringBasedR2dbcQuery.java | 67 +----- .../query/StringBasedR2dbcQueryUnitTests.java | 121 ++++++++++- 6 files changed, 521 insertions(+), 59 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionEvaluatingParameterBinder.java create mode 100644 src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionQuery.java diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 78ed3980..6f468840 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -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 diff --git a/src/main/asciidoc/reference/r2dbc-repositories.adoc b/src/main/asciidoc/reference/r2dbc-repositories.adoc index a5d17f2d..dc25c089 100644 --- a/src/main/asciidoc/reference/r2dbc-repositories.adoc +++ b/src/main/asciidoc/reference/r2dbc-repositories.adoc @@ -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 { + + @Query("SELECT * FROM person WHERE lastname = :#{[0]} }") + List 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 diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionEvaluatingParameterBinder.java b/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionEvaluatingParameterBinder.java new file mode 100644 index 00000000..c940628e --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionEvaluatingParameterBinder.java @@ -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 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 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 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 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 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 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); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionQuery.java new file mode 100644 index 00000000..4065f3ae --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionQuery.java @@ -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 parameterBindings; + + private ExpressionQuery(String query, List 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 parameterBindings = new ArrayList<>(); + String rewritten = transformQueryAndCollectExpressionParametersIntoBindings(query, parameterBindings); + + return new ExpressionQuery(rewritten, parameterBindings); + } + + public String getQuery() { + return query; + } + + public List getBindings() { + return parameterBindings; + } + + private static String transformQueryAndCollectExpressionParametersIntoBindings(String input, + List 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; + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index afe24b93..7ccc9b33 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -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. *

- * 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 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 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 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 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(); } }; } + } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index c45d39a2..efb4ed3a 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -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 { @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; + } } }