Browse Source

Polishing.

Simplify R2DBC expression handling. Use new ValueExpression API instead of holding parameter binding duplicates.

Reformat code. Add author tags.

See #1904
Original pull request: #1906
pull/1912/head
Mark Paluch 1 year ago
parent
commit
8c6364b4bf
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 51
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java
  2. 10
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java
  3. 5
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java
  4. 78
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/DefaultR2dbcSpELExpressionEvaluator.java
  5. 30
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionEvaluatingParameterBinder.java
  6. 52
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionQuery.java
  7. 35
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcSpELExpressionEvaluator.java
  8. 39
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java
  9. 3
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java
  10. 18
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/ExpressionQueryUnitTests.java

51
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java

@ -20,18 +20,17 @@ import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.sql.SQLType; import java.sql.SQLType;
import java.util.AbstractMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes;
@ -51,7 +50,6 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
import org.springframework.data.util.Lazy; import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
@ -74,6 +72,7 @@ import org.springframework.util.ObjectUtils;
* @author Chirag Tailor * @author Chirag Tailor
* @author Christopher Klein * @author Christopher Klein
* @author Mikhail Polivakha * @author Mikhail Polivakha
* @author Marcin Grzejszczak
* @since 2.0 * @since 2.0
*/ */
public class StringBasedJdbcQuery extends AbstractJdbcQuery { public class StringBasedJdbcQuery extends AbstractJdbcQuery {
@ -82,13 +81,11 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
private final JdbcConverter converter; private final JdbcConverter converter;
private final RowMapperFactory rowMapperFactory; private final RowMapperFactory rowMapperFactory;
private final ValueExpressionQueryRewriter.ParsedQuery parsedQuery; private final ValueExpressionQueryRewriter.ParsedQuery parsedQuery;
private final boolean containsSpelExpressions;
private final String query; private final String query;
private final CachedRowMapperFactory cachedRowMapperFactory; private final CachedRowMapperFactory cachedRowMapperFactory;
private final CachedResultSetExtractorFactory cachedResultSetExtractorFactory; private final CachedResultSetExtractorFactory cachedResultSetExtractorFactory;
private final ValueExpressionDelegate delegate; private final ValueExpressionDelegate delegate;
private final List<Map.Entry<String, String>> parameterBindings;
/** /**
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
@ -186,17 +183,11 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
this.cachedResultSetExtractorFactory = new CachedResultSetExtractorFactory( this.cachedResultSetExtractorFactory = new CachedResultSetExtractorFactory(
this.cachedRowMapperFactory::getRowMapper); this.cachedRowMapperFactory::getRowMapper);
this.parameterBindings = new ArrayList<>(); ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(delegate,
(counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat);
ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(delegate, (counter, expression) -> {
String newName = String.format("__$synthetic$__%d", counter + 1);
parameterBindings.add(new AbstractMap.SimpleEntry<>(newName, expression));
return newName;
}, String::concat);
this.query = query; this.query = query;
this.parsedQuery = rewriter.parse(this.query); this.parsedQuery = rewriter.parse(this.query);
this.containsSpelExpressions = !this.parsedQuery.getQueryString().equals(this.query);
this.delegate = delegate; this.delegate = delegate;
} }
@ -217,9 +208,10 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
RowMapperFactory rowMapperFactory, JdbcConverter converter, RowMapperFactory rowMapperFactory, JdbcConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider) { QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(query, queryMethod, operations, rowMapperFactory, converter, new CachingValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(null, this(query, queryMethod, operations, rowMapperFactory, converter, new CachingValueExpressionDelegate(
rootObject -> evaluationContextProvider.getEvaluationContext(queryMethod.getParameters(), new Object[] { rootObject })), ValueExpressionParser.create( new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), rootObject -> evaluationContextProvider
SpelExpressionParser::new))); .getEvaluationContext(queryMethod.getParameters(), new Object[] { rootObject })),
ValueExpressionParser.create()));
} }
@Override @Override
@ -231,18 +223,22 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
JdbcQueryExecution<?> queryExecution = createJdbcQueryExecution(accessor, processor); JdbcQueryExecution<?> queryExecution = createJdbcQueryExecution(accessor, processor);
MapSqlParameterSource parameterMap = this.bindParameters(accessor); MapSqlParameterSource parameterMap = this.bindParameters(accessor);
return queryExecution.execute(processSpelExpressions(objects, accessor.getBindableParameters(), parameterMap), parameterMap); return queryExecution.execute(evaluateExpressions(objects, accessor.getBindableParameters(), parameterMap),
parameterMap);
} }
private String processSpelExpressions(Object[] objects, Parameters<?, ?> bindableParameters, MapSqlParameterSource parameterMap) { private String evaluateExpressions(Object[] objects, Parameters<?, ?> bindableParameters,
MapSqlParameterSource parameterMap) {
if (parsedQuery.hasParameterBindings()) {
if (containsSpelExpressions) {
ValueEvaluationContext evaluationContext = delegate.createValueContextProvider(bindableParameters) ValueEvaluationContext evaluationContext = delegate.createValueContextProvider(bindableParameters)
.getEvaluationContext(objects); .getEvaluationContext(objects);
for (Map.Entry<String, String> entry : parameterBindings) {
parameterMap.addValue( parsedQuery.getParameterMap().forEach((paramName, valueExpression) -> {
entry.getKey(), delegate.parse(entry.getValue()).evaluate(evaluationContext)); parameterMap.addValue(paramName, valueExpression.evaluate(evaluationContext));
} });
return parsedQuery.getQueryString(); return parsedQuery.getQueryString();
} }
@ -254,13 +250,12 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
if (getQueryMethod().isModifyingQuery()) { if (getQueryMethod().isModifyingQuery()) {
return createModifyingQueryExecutor(); return createModifyingQueryExecutor();
} else { }
Supplier<RowMapper<?>> rowMapper = () -> determineRowMapper(processor, accessor.findDynamicProjection() != null); Supplier<RowMapper<?>> rowMapper = () -> determineRowMapper(processor, accessor.findDynamicProjection() != null);
ResultSetExtractor<Object> resultSetExtractor = determineResultSetExtractor(rowMapper); ResultSetExtractor<Object> resultSetExtractor = determineResultSetExtractor(rowMapper);
return createReadingQueryExecution(resultSetExtractor, rowMapper); return createReadingQueryExecution(resultSetExtractor, rowMapper);
}
} }
private MapSqlParameterSource bindParameters(RelationalParameterAccessor accessor) { private MapSqlParameterSource bindParameters(RelationalParameterAccessor accessor) {

10
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java

@ -32,6 +32,7 @@ import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.PersistentEntityInformation; import org.springframework.data.repository.core.support.PersistentEntityInformation;
import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.CachingValueExpressionDelegate;
import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@ -48,6 +49,7 @@ import org.springframework.util.Assert;
* @author Hebert Coelho * @author Hebert Coelho
* @author Diego Krupitza * @author Diego Krupitza
* @author Christopher Klein * @author Christopher Klein
* @author Marcin Grzejszczak
*/ */
public class JdbcRepositoryFactory extends RepositoryFactorySupport { public class JdbcRepositoryFactory extends RepositoryFactorySupport {
@ -57,7 +59,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
private final DataAccessStrategy accessStrategy; private final DataAccessStrategy accessStrategy;
private final NamedParameterJdbcOperations operations; private final NamedParameterJdbcOperations operations;
private final Dialect dialect; private final Dialect dialect;
@Nullable private BeanFactory beanFactory; private @Nullable BeanFactory beanFactory;
private QueryMappingConfiguration queryMappingConfiguration = QueryMappingConfiguration.EMPTY; private QueryMappingConfiguration queryMappingConfiguration = QueryMappingConfiguration.EMPTY;
private EntityCallbacks entityCallbacks; private EntityCallbacks entityCallbacks;
@ -132,10 +134,12 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
return SimpleJdbcRepository.class; return SimpleJdbcRepository.class;
} }
@Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrategy.Key key, @Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable QueryLookupStrategy.Key key,
ValueExpressionDelegate valueExpressionDelegate) { ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(JdbcQueryLookupStrategy.create(key, publisher, entityCallbacks, context, converter, dialect, return Optional.of(JdbcQueryLookupStrategy.create(key, publisher, entityCallbacks, context, converter, dialect,
queryMappingConfiguration, operations, beanFactory, valueExpressionDelegate)); queryMappingConfiguration, operations, beanFactory,
new CachingValueExpressionDelegate(valueExpressionDelegate)));
} }
/** /**

5
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java

@ -34,7 +34,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
@ -79,6 +78,7 @@ import org.springframework.util.ReflectionUtils;
* @author Dennis Effing * @author Dennis Effing
* @author Chirag Tailor * @author Chirag Tailor
* @author Christopher Klein * @author Christopher Klein
* @author Marcin Grzejszczak
*/ */
class StringBasedJdbcQueryUnitTests { class StringBasedJdbcQueryUnitTests {
@ -95,8 +95,7 @@ class StringBasedJdbcQueryUnitTests {
this.operations = mock(NamedParameterJdbcOperations.class); this.operations = mock(NamedParameterJdbcOperations.class);
this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS); this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS);
this.converter = new MappingJdbcConverter(context, mock(RelationResolver.class)); this.converter = new MappingJdbcConverter(context, mock(RelationResolver.class));
QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), new StaticApplicationContext()); this.delegate = ValueExpressionDelegate.create();
this.delegate = new ValueExpressionDelegate(accessor, ValueExpressionParser.create());
} }
@Test // DATAJDBC-165 @Test // DATAJDBC-165

78
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/DefaultR2dbcSpELExpressionEvaluator.java

@ -1,78 +0,0 @@
/*
* Copyright 2020-2024 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 org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.r2dbc.core.Parameter;
/**
* Simple {@link R2dbcSpELExpressionEvaluator} implementation using {@link ExpressionParser} and
* {@link EvaluationContext}.
*
* @author Mark Paluch
* @since 1.2
*/
class DefaultR2dbcSpELExpressionEvaluator implements R2dbcSpELExpressionEvaluator {
private final ValueExpressionDelegate delegate;
private final ValueEvaluationContext context;
DefaultR2dbcSpELExpressionEvaluator(ValueExpressionDelegate delegate, ValueEvaluationContext context) {
this.delegate = delegate;
this.context = context;
}
/**
* Return a {@link SpELExpressionEvaluator} that does not support expression evaluation.
*
* @return a {@link SpELExpressionEvaluator} that does not support expression evaluation.
*/
public static R2dbcSpELExpressionEvaluator unsupported() {
return NoOpExpressionEvaluator.INSTANCE;
}
@Override
public Parameter evaluate(String expression) {
ValueExpression expr = delegate.parse(expression);
Object value = expr.evaluate(context);
Class<?> valueType = value != null ? value.getClass() : Object.class;
return org.springframework.r2dbc.core.Parameter.fromOrEmpty(value, valueType);
}
/**
* {@link SpELExpressionEvaluator} that does not support SpEL evaluation.
*
* @author Mark Paluch
*/
enum NoOpExpressionEvaluator implements R2dbcSpELExpressionEvaluator {
INSTANCE;
@Override
public Parameter evaluate(String expression) {
throw new UnsupportedOperationException("Expression evaluation not supported");
}
}
}

30
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionEvaluatingParameterBinder.java

@ -15,13 +15,13 @@
*/ */
package org.springframework.data.r2dbc.repository.query; package org.springframework.data.r2dbc.repository.query;
import static org.springframework.data.r2dbc.repository.query.ExpressionQuery.*;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.dialect.BindTargetBinder; import org.springframework.data.r2dbc.dialect.BindTargetBinder;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
@ -64,26 +64,40 @@ class ExpressionEvaluatingParameterBinder {
* @param evaluator must not be {@literal null}. * @param evaluator must not be {@literal null}.
*/ */
void bind(BindTarget bindTarget, void bind(BindTarget bindTarget,
RelationalParameterAccessor parameterAccessor, R2dbcSpELExpressionEvaluator evaluator) { RelationalParameterAccessor parameterAccessor, ValueEvaluationContext evaluationContext) {
Object[] values = parameterAccessor.getValues(); Object[] values = parameterAccessor.getValues();
Parameters<?, ?> bindableParameters = parameterAccessor.getBindableParameters(); Parameters<?, ?> bindableParameters = parameterAccessor.getBindableParameters();
bindExpressions(bindTarget, evaluator); bindExpressions(bindTarget, evaluationContext);
bindParameters(bindTarget, parameterAccessor.hasBindableNullValue(), values, bindableParameters); bindParameters(bindTarget, parameterAccessor.hasBindableNullValue(), values, bindableParameters);
} }
private void bindExpressions(BindTarget bindSpec, private void bindExpressions(BindTarget bindSpec,
R2dbcSpELExpressionEvaluator evaluator) { ValueEvaluationContext evaluationContext) {
BindTargetBinder binder = new BindTargetBinder(bindSpec); BindTargetBinder binder = new BindTargetBinder(bindSpec);
for (ParameterBinding binding : expressionQuery.getBindings()) {
expressionQuery.getBindings().forEach((paramName, valueExpression) -> {
org.springframework.r2dbc.core.Parameter valueForBinding = getBindValue( org.springframework.r2dbc.core.Parameter valueForBinding = getBindValue(
evaluator.evaluate(binding.getExpression())); evaluate(valueExpression, evaluationContext));
binder.bind(paramName, valueForBinding);
});
}
private org.springframework.r2dbc.core.Parameter evaluate(ValueExpression expression,
ValueEvaluationContext context) {
binder.bind(binding.getParameterName(), valueForBinding); Object value = expression.evaluate(context);
Class<?> valueType = value != null ? value.getClass() : null;
if (valueType == null) {
valueType = expression.getValueType(context);
} }
return org.springframework.r2dbc.core.Parameter.fromOrEmpty(value, valueType == null ? Object.class : valueType);
} }
private void bindParameters(BindTarget bindSpec, private void bindParameters(BindTarget bindSpec,

52
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionQuery.java

@ -15,9 +15,9 @@
*/ */
package org.springframework.data.r2dbc.repository.query; package org.springframework.data.r2dbc.repository.query;
import java.util.ArrayList; import java.util.Map;
import java.util.List;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
@ -34,13 +34,11 @@ class ExpressionQuery {
private static final String SYNTHETIC_PARAMETER_TEMPLATE = "__synthetic_%d__"; private static final String SYNTHETIC_PARAMETER_TEMPLATE = "__synthetic_%d__";
private final String query; private final String query;
private final Map<String, ValueExpression> parameterMap;
private final List<ParameterBinding> parameterBindings; private ExpressionQuery(String query, Map<String, ValueExpression> parameterMap) {
private ExpressionQuery(String query, List<ParameterBinding> parameterBindings) {
this.query = query; this.query = query;
this.parameterBindings = parameterBindings; this.parameterMap = parameterMap;
} }
/** /**
@ -51,55 +49,25 @@ class ExpressionQuery {
*/ */
public static ExpressionQuery create(ValueExpressionParser parser, String query) { public static ExpressionQuery create(ValueExpressionParser parser, String query) {
List<ParameterBinding> parameterBindings = new ArrayList<>();
ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(parser, (counter, expression) -> {
String parameterName = String.format(SYNTHETIC_PARAMETER_TEMPLATE, counter);
parameterBindings.add(new ParameterBinding(parameterName, expression));
return parameterName;
}, String::concat);
ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(parser,
(counter, expression) -> String.format(SYNTHETIC_PARAMETER_TEMPLATE, counter), String::concat);
ValueExpressionQueryRewriter.ParsedQuery parsed = rewriter.parse(query); ValueExpressionQueryRewriter.ParsedQuery parsed = rewriter.parse(query);
return new ExpressionQuery(parsed.getQueryString(), parameterBindings); return new ExpressionQuery(parsed.getQueryString(), parsed.getParameterMap());
} }
public String getQuery() { public String getQuery() {
return query; return query;
} }
public List<ParameterBinding> getBindings() { public Map<String, ValueExpression> getBindings() {
return parameterBindings; return parameterMap;
} }
@Override @Override
public String toString() { public String toString() {
return query; 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;
}
String getExpression() {
return expression;
}
String getParameterName() {
return parameterName;
}
}
} }

35
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcSpELExpressionEvaluator.java

@ -1,35 +0,0 @@
/*
* Copyright 2020-2024 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 org.springframework.r2dbc.core.Parameter;
/**
* SPI for components that can evaluate Spring EL expressions and return {@link Parameter}.
*
* @author Mark Paluch
* @since 1.2
*/
interface R2dbcSpELExpressionEvaluator {
/**
* Evaluates the given expression.
*
* @param expression
* @return
*/
Parameter evaluate(String expression);
}

39
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java

@ -24,6 +24,7 @@ import java.util.Map;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ReactiveValueEvaluationContextProvider; import org.springframework.data.expression.ReactiveValueEvaluationContextProvider;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.convert.R2dbcConverter;
@ -53,6 +54,7 @@ import org.springframework.util.Assert;
* named parameters (if enabled on {@link DatabaseClient}) and SpEL expressions enclosed with {@code :#{}}. * named parameters (if enabled on {@link DatabaseClient}) and SpEL expressions enclosed with {@code :#{}}.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Marcin Grzejszczak
*/ */
public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
@ -60,8 +62,7 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
private final ExpressionEvaluatingParameterBinder binder; private final ExpressionEvaluatingParameterBinder binder;
private final ExpressionDependencies expressionDependencies; private final ExpressionDependencies expressionDependencies;
private final ReactiveDataAccessStrategy dataAccessStrategy; private final ReactiveDataAccessStrategy dataAccessStrategy;
private final ValueExpressionDelegate valueExpressionDelegate; private final ReactiveValueEvaluationContextProvider valueContextProvider;
private final ValueEvaluationContextProvider valueContextProvider;
/** /**
* Creates a new {@link StringBasedR2dbcQuery} for the given {@link StringBasedR2dbcQuery}, {@link DatabaseClient}, * Creates a new {@link StringBasedR2dbcQuery} for the given {@link StringBasedR2dbcQuery}, {@link DatabaseClient},
@ -132,17 +133,22 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ValueExpressionDelegate valueExpressionDelegate) { R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ValueExpressionDelegate valueExpressionDelegate) {
super(method, entityOperations, converter); super(method, entityOperations, converter);
this.valueExpressionDelegate = valueExpressionDelegate;
Assert.hasText(query, "Query must not be empty"); Assert.hasText(query, "Query must not be empty");
this.dataAccessStrategy = dataAccessStrategy; this.dataAccessStrategy = dataAccessStrategy;
this.expressionQuery = ExpressionQuery.create(valueExpressionDelegate, query); this.expressionQuery = ExpressionQuery.create(valueExpressionDelegate, query);
this.binder = new ExpressionEvaluatingParameterBinder(expressionQuery, dataAccessStrategy); this.binder = new ExpressionEvaluatingParameterBinder(expressionQuery, dataAccessStrategy);
this.valueContextProvider = valueExpressionDelegate.createValueContextProvider(
method.getParameters()); ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate
.createValueContextProvider(method.getParameters());
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider,
"ValueEvaluationContextProvider must be reactive");
this.valueContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider;
this.expressionDependencies = createExpressionDependencies(); this.expressionDependencies = createExpressionDependencies();
if (method.isSliceQuery()) { if (method.isSliceQuery()) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Slice queries are not supported using string-based queries; Offending method: " + method); "Slice queries are not supported using string-based queries; Offending method: " + method);
@ -167,9 +173,8 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
List<ExpressionDependencies> dependencies = new ArrayList<>(); List<ExpressionDependencies> dependencies = new ArrayList<>();
for (ExpressionQuery.ParameterBinding binding : expressionQuery.getBindings()) { expressionQuery.getBindings()
dependencies.add(valueExpressionDelegate.parse(binding.getExpression()).getExpressionDependencies()); .forEach((s, valueExpression) -> dependencies.add(valueExpression.getExpressionDependencies()));
}
return ExpressionDependencies.merged(dependencies); return ExpressionDependencies.merged(dependencies);
} }
@ -191,7 +196,7 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
@Override @Override
protected Mono<PreparedOperation<?>> createQuery(RelationalParameterAccessor accessor) { protected Mono<PreparedOperation<?>> createQuery(RelationalParameterAccessor accessor) {
return getSpelEvaluator(accessor).map(evaluator -> new ExpandedQuery(accessor, evaluator)); return getExpressionEvaluator(accessor).map(evaluator -> new ExpandedQuery(accessor, evaluator));
} }
@Override @Override
@ -201,19 +206,13 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
return !returnedType.isInterface() ? returnedType : super.resolveResultType(resultProcessor); return !returnedType.isInterface() ? returnedType : super.resolveResultType(resultProcessor);
} }
private Mono<R2dbcSpELExpressionEvaluator> getSpelEvaluator(RelationalParameterAccessor accessor) { private Mono<ValueEvaluationContext> getExpressionEvaluator(RelationalParameterAccessor accessor) {
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, "ValueEvaluationContextProvider must be reactive"); return valueContextProvider.getEvaluationContextLater(accessor.getValues(), expressionDependencies);
return ((ReactiveValueEvaluationContextProvider) valueContextProvider)
.getEvaluationContextLater(accessor.getValues(), expressionDependencies)
.<R2dbcSpELExpressionEvaluator> map(
context -> new DefaultR2dbcSpELExpressionEvaluator(valueExpressionDelegate, context))
.defaultIfEmpty(DefaultR2dbcSpELExpressionEvaluator.unsupported());
} }
@Override @Override
public String toString() { public String toString() {
String sb = getClass().getSimpleName() + " [" + expressionQuery.getQuery() + ']'; return getClass().getSimpleName() + " [" + expressionQuery.getQuery() + ']';
return sb;
} }
private class ExpandedQuery implements PreparedOperation<String> { private class ExpandedQuery implements PreparedOperation<String> {
@ -226,10 +225,10 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
private final Map<Integer, Parameter> remainderByIndex; private final Map<Integer, Parameter> remainderByIndex;
public ExpandedQuery(RelationalParameterAccessor accessor, R2dbcSpELExpressionEvaluator evaluator) { public ExpandedQuery(RelationalParameterAccessor accessor, ValueEvaluationContext evaluationContext) {
this.recordedBindings = new BindTargetRecorder(); this.recordedBindings = new BindTargetRecorder();
binder.bind(recordedBindings, accessor, evaluator); binder.bind(recordedBindings, accessor, evaluationContext);
remainderByName = new LinkedHashMap<>(recordedBindings.byName); remainderByName = new LinkedHashMap<>(recordedBindings.byName);
remainderByIndex = new LinkedHashMap<>(recordedBindings.byIndex); remainderByIndex = new LinkedHashMap<>(recordedBindings.byIndex);

3
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java

@ -52,6 +52,7 @@ import org.springframework.util.Assert;
* *
* @author Mark Paluch * @author Mark Paluch
* @author Jens Schauder * @author Jens Schauder
* @author Marcin Grzejszczak
*/ */
public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
@ -113,7 +114,7 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
} }
@Override @Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key, protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
ValueExpressionDelegate valueExpressionDelegate) { ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(new R2dbcQueryLookupStrategy(operations, new CachingValueExpressionDelegate(valueExpressionDelegate), converter, dataAccessStrategy)); return Optional.of(new R2dbcQueryLookupStrategy(operations, new CachingValueExpressionDelegate(valueExpressionDelegate), converter, dataAccessStrategy));
} }

18
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/ExpressionQueryUnitTests.java

@ -17,8 +17,11 @@ package org.springframework.data.r2dbc.repository.query;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.expression.ValueExpressionParser;
/** /**
@ -26,11 +29,12 @@ import org.springframework.data.expression.ValueExpressionParser;
* *
* @author Mark Paluch * @author Mark Paluch
* @author Jens Schauder * @author Jens Schauder
* @author Marcin Grzejszczak
*/ */
class ExpressionQueryUnitTests { class ExpressionQueryUnitTests {
@Test // gh-373 @Test // gh-373, gh-1904
void bindsMultipleSpelParametersCorrectly() { void bindsMultipleExpressionParametersCorrectly() {
ExpressionQuery query = ExpressionQuery ExpressionQuery query = ExpressionQuery
.create(ValueExpressionParser.create(), "INSERT IGNORE INTO table (x, y) VALUES (:#{#point.x}, :${point.y})"); .create(ValueExpressionParser.create(), "INSERT IGNORE INTO table (x, y) VALUES (:#{#point.x}, :${point.y})");
@ -38,10 +42,10 @@ class ExpressionQueryUnitTests {
assertThat(query.getQuery()) assertThat(query.getQuery())
.isEqualTo("INSERT IGNORE INTO table (x, y) VALUES (:__synthetic_0__, :__synthetic_1__)"); .isEqualTo("INSERT IGNORE INTO table (x, y) VALUES (:__synthetic_0__, :__synthetic_1__)");
assertThat(query.getBindings()).hasSize(2); Map<String, ValueExpression> bindings = query.getBindings();
assertThat(query.getBindings().get(0).getExpression()).isEqualTo("#{#point.x}"); assertThat(bindings).hasSize(2);
assertThat(query.getBindings().get(0).getParameterName()).isEqualTo("__synthetic_0__");
assertThat(query.getBindings().get(1).getExpression()).isEqualTo("${point.y}"); assertThat(bindings.get("__synthetic_0__").getExpressionString()).isEqualTo("#point.x");
assertThat(query.getBindings().get(1).getParameterName()).isEqualTo("__synthetic_1__"); assertThat(bindings.get("__synthetic_1__").getExpressionString()).isEqualTo("${point.y}");
} }
} }

Loading…
Cancel
Save