Skip to content

Commit 6027e29

Browse files
committed
#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); }
1 parent 714dec3 commit 6027e29

File tree

6 files changed

+521
-59
lines changed

6 files changed

+521
-59
lines changed

Diff for: src/main/asciidoc/new-features.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* `@Modifying` annotation for query methods to consume affected row count.
1515
* Repository `save(…)` with an associated Id terminates with `TransientDataAccessException` if the row does not exist in the database.
1616
* Added `SingleConnectionConnectionFactory` for testing using connection singletons.
17+
* Support for {spring-framework-ref}/core.html#expressions[SpEL expressions] in `@Query`.
1718

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

Diff for: src/main/asciidoc/reference/r2dbc-repositories.adoc

+29
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,35 @@ The result of a modifying query can be:
149149
The `@Modifying` annotation is only relevant in combination with the `@Query` annotation.
150150
Derived custom methods do not require this annotation.
151151

152+
[[r2dbc.repositories.queries.spel]]
153+
=== Queries with SpEL Expressions
154+
155+
Query string definitions can be used together with SpEL expressions to create dynamic queries at runtime.
156+
SpEL expressions can provide predicate values which are evaluated right before executing the query.
157+
158+
Expressions expose method arguments through an array that contains all the arguments.
159+
The following query uses `[0]`
160+
to declare the predicate value for `lastname` (which is equivalent to the `:lastname` parameter binding):
161+
162+
[source,java]
163+
----
164+
public interface PersonRepository extends ReactiveCrudRepository<Person, String> {
165+
166+
@Query("SELECT * FROM person WHERE lastname = :#{[0]} }")
167+
List<Person> findByQueryWithExpression(String lastname);
168+
}
169+
----
170+
171+
SpEL in query strings can be a powerful way to enhance queries.
172+
However, they can also accept a broad range of unwanted arguments.
173+
You should make sure to sanitize strings before passing them to the query to avoid unwanted changes to your query.
174+
175+
Expression support is extensible through the Query SPI: `org.springframework.data.spel.spi.EvaluationContextExtension`.
176+
The Query SPI can contribute properties and functions and can customize the root object.
177+
Extensions are retrieved from the application context at the time of SpEL evaluation when the query is built.
178+
179+
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.
180+
152181
[[r2dbc.entity-persistence.state-detection-strategies]]
153182
=== Entity State Detection Strategies
154183

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright 2020 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+
* https://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.repository.query;
17+
18+
import static org.springframework.data.r2dbc.repository.query.ExpressionQuery.*;
19+
20+
import java.util.Map;
21+
import java.util.Optional;
22+
import java.util.concurrent.ConcurrentHashMap;
23+
import java.util.regex.Pattern;
24+
25+
import org.springframework.data.r2dbc.core.DatabaseClient;
26+
import org.springframework.data.r2dbc.mapping.SettableValue;
27+
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
28+
import org.springframework.data.repository.query.Parameter;
29+
import org.springframework.data.repository.query.Parameters;
30+
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
31+
import org.springframework.expression.EvaluationContext;
32+
import org.springframework.expression.Expression;
33+
import org.springframework.expression.spel.standard.SpelExpressionParser;
34+
import org.springframework.util.Assert;
35+
36+
/**
37+
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a
38+
* {@link String}.
39+
*
40+
* @author Mark Paluch
41+
* @since 1.1
42+
*/
43+
class ExpressionEvaluatingParameterBinder {
44+
45+
private final SpelExpressionParser expressionParser;
46+
47+
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
48+
49+
private final ExpressionQuery expressionQuery;
50+
51+
private final Map<String, Boolean> namedParameters = new ConcurrentHashMap<>();
52+
53+
/**
54+
* Creates new {@link ExpressionEvaluatingParameterBinder}
55+
*
56+
* @param expressionParser must not be {@literal null}.
57+
* @param evaluationContextProvider must not be {@literal null}.
58+
* @param expressionQuery must not be {@literal null}.
59+
*/
60+
ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser,
61+
QueryMethodEvaluationContextProvider evaluationContextProvider, ExpressionQuery expressionQuery) {
62+
63+
Assert.notNull(expressionParser, "ExpressionParser must not be null");
64+
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null");
65+
Assert.notNull(expressionQuery, "ExpressionQuery must not be null");
66+
67+
this.expressionParser = expressionParser;
68+
this.evaluationContextProvider = evaluationContextProvider;
69+
this.expressionQuery = expressionQuery;
70+
}
71+
72+
/**
73+
* Bind values provided by {@link RelationalParameterAccessor} to placeholders in {@link ExpressionQuery} while
74+
* considering potential conversions and parameter types.
75+
*
76+
* @param bindSpec must not be {@literal null}.
77+
* @param parameterAccessor must not be {@literal null}.
78+
*/
79+
public <T extends DatabaseClient.BindSpec<T>> T bind(T bindSpec, RelationalParameterAccessor parameterAccessor) {
80+
81+
Object[] values = parameterAccessor.getValues();
82+
Parameters<?, ?> bindableParameters = parameterAccessor.getBindableParameters();
83+
84+
T bindSpecToUse = bindExpressions(bindSpec, values, bindableParameters);
85+
bindSpecToUse = bindParameters(bindSpecToUse, parameterAccessor.hasBindableNullValue(), values, bindableParameters);
86+
87+
return bindSpecToUse;
88+
}
89+
90+
private <T extends DatabaseClient.BindSpec<T>> T bindExpressions(T bindSpec, Object[] values,
91+
Parameters<?, ?> bindableParameters) {
92+
93+
T bindSpecToUse = bindSpec;
94+
95+
for (ParameterBinding binding : expressionQuery.getBindings()) {
96+
97+
SettableValue valueForBinding = getParameterValueForBinding(bindableParameters, values, binding);
98+
99+
if (valueForBinding.isEmpty()) {
100+
bindSpecToUse = bindSpecToUse.bindNull(binding.getParameterName(), valueForBinding.getType());
101+
} else {
102+
bindSpecToUse = bindSpecToUse.bind(binding.getParameterName(), valueForBinding.getValue());
103+
}
104+
}
105+
106+
return bindSpecToUse;
107+
}
108+
109+
private <T extends DatabaseClient.BindSpec<T>> T bindParameters(T bindSpec, boolean bindableNull, Object[] values,
110+
Parameters<?, ?> bindableParameters) {
111+
112+
T bindSpecToUse = bindSpec;
113+
int index = 0;
114+
int bindingIndex = 0;
115+
116+
for (Object value : values) {
117+
118+
Parameter bindableParameter = bindableParameters.getBindableParameter(index++);
119+
120+
Optional<String> name = bindableParameter.getName();
121+
122+
if ((name.isPresent() && isNamedParameterUsed(name)) || !expressionQuery.getBindings().isEmpty()) {
123+
124+
if (isNamedParameterUsed(name)) {
125+
126+
if (value == null) {
127+
if (bindableNull) {
128+
bindSpecToUse = bindSpecToUse.bindNull(name.get(), bindableParameter.getType());
129+
}
130+
} else {
131+
bindSpecToUse = bindSpecToUse.bind(name.get(), value);
132+
}
133+
}
134+
135+
// skip unused named parameters if there is SpEL
136+
} else {
137+
if (value == null) {
138+
if (bindableNull) {
139+
bindSpecToUse = bindSpecToUse.bindNull(bindingIndex++, bindableParameter.getType());
140+
}
141+
} else {
142+
bindSpecToUse = bindSpecToUse.bind(bindingIndex++, value);
143+
}
144+
}
145+
}
146+
147+
return bindSpecToUse;
148+
}
149+
150+
private boolean isNamedParameterUsed(Optional<String> name) {
151+
152+
if (!name.isPresent()) {
153+
return false;
154+
}
155+
156+
return namedParameters.computeIfAbsent(name.get(), it -> {
157+
158+
Pattern namedParameterPattern = Pattern.compile("(\\W)[:#$@]" + Pattern.quote(it) + "(\\W|$)");
159+
return namedParameterPattern.matcher(expressionQuery.getQuery()).find();
160+
});
161+
}
162+
163+
/**
164+
* Returns the value to be used for the given {@link ParameterBinding}.
165+
*
166+
* @param parameters must not be {@literal null}.
167+
* @param binding must not be {@literal null}.
168+
* @return the value used for the given {@link ParameterBinding}.
169+
*/
170+
private SettableValue getParameterValueForBinding(Parameters<?, ?> parameters, Object[] values,
171+
ParameterBinding binding) {
172+
return evaluateExpression(binding.getExpression(), parameters, values);
173+
}
174+
175+
/**
176+
* Evaluates the given {@code expressionString}.
177+
*
178+
* @param expressionString must not be {@literal null} or empty.
179+
* @param parameters must not be {@literal null}.
180+
* @param parameterValues must not be {@literal null}.
181+
* @return the value of the {@code expressionString} evaluation.
182+
*/
183+
private SettableValue evaluateExpression(String expressionString, Parameters<?, ?> parameters,
184+
Object[] parameterValues) {
185+
186+
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues);
187+
Expression expression = expressionParser.parseExpression(expressionString);
188+
189+
Object value = expression.getValue(evaluationContext, Object.class);
190+
Class<?> valueType = expression.getValueType(evaluationContext);
191+
192+
return SettableValue.fromOrEmpty(value, valueType != null ? valueType : Object.class);
193+
}
194+
}

0 commit comments

Comments
 (0)