Add support for Value Expressions for Repository Query methods.

Closes #1904
Original pull request: #1906
This commit is contained in:
Marcin Grzejszczak
2024-10-03 17:45:09 +02:00
committed by Mark Paluch
parent fd4aedc760
commit d526cd3a22
13 changed files with 218 additions and 164 deletions

View File

@@ -15,9 +15,11 @@
*/
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.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.r2dbc.core.Parameter;
@@ -30,12 +32,12 @@ import org.springframework.r2dbc.core.Parameter;
*/
class DefaultR2dbcSpELExpressionEvaluator implements R2dbcSpELExpressionEvaluator {
private final ExpressionParser parser;
private final ValueExpressionDelegate delegate;
private final EvaluationContext context;
private final ValueEvaluationContext context;
DefaultR2dbcSpELExpressionEvaluator(ExpressionParser parser, EvaluationContext context) {
this.parser = parser;
DefaultR2dbcSpELExpressionEvaluator(ValueExpressionDelegate delegate, ValueEvaluationContext context) {
this.delegate = delegate;
this.context = context;
}
@@ -51,12 +53,12 @@ class DefaultR2dbcSpELExpressionEvaluator implements R2dbcSpELExpressionEvaluato
@Override
public Parameter evaluate(String expression) {
Expression expr = parser.parseExpression(expression);
ValueExpression expr = delegate.parse(expression);
Object value = expr.getValue(context, Object.class);
Class<?> valueType = expr.getValueType(context);
Object value = expr.evaluate(context);
Class<?> valueType = value != null ? value.getClass() : Object.class;
return org.springframework.r2dbc.core.Parameter.fromOrEmpty(value, valueType != null ? valueType : Object.class);
return org.springframework.r2dbc.core.Parameter.fromOrEmpty(value, valueType);
}
/**

View File

@@ -18,7 +18,8 @@ package org.springframework.data.r2dbc.repository.query;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.repository.query.SpelQueryContext;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
/**
* Query using Spring Expression Language to indicate parameter bindings. Queries using SpEL use {@code :#{…}} to
@@ -48,18 +49,17 @@ class ExpressionQuery {
* @param query the query string to parse.
* @return the parsed {@link ExpressionQuery}.
*/
public static ExpressionQuery create(String query) {
public static ExpressionQuery create(ValueExpressionParser parser, String query) {
List<ParameterBinding> parameterBindings = new ArrayList<>();
SpelQueryContext queryContext = SpelQueryContext.of((counter, expression) -> {
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);
SpelQueryContext.SpelExtractor parsed = queryContext.parse(query);
ValueExpressionQueryRewriter.ParsedQuery parsed = rewriter.parse(query);
return new ExpressionQuery(parsed.getQueryString(), parameterBindings);
}

View File

@@ -22,6 +22,10 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.expression.ReactiveValueEvaluationContextProvider;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
@@ -29,8 +33,10 @@ import org.springframework.data.r2dbc.dialect.BindTargetBinder;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -52,10 +58,10 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
private final ExpressionQuery expressionQuery;
private final ExpressionEvaluatingParameterBinder binder;
private final ExpressionParser expressionParser;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final ExpressionDependencies expressionDependencies;
private final ReactiveDataAccessStrategy dataAccessStrategy;
private final ValueExpressionDelegate valueExpressionDelegate;
private final ValueEvaluationContextProvider valueContextProvider;
/**
* Creates a new {@link StringBasedR2dbcQuery} for the given {@link StringBasedR2dbcQuery}, {@link DatabaseClient},
@@ -67,7 +73,9 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
* @param dataAccessStrategy must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated use the constructor version with {@link ValueExpressionDelegate}
*/
@Deprecated(since = "3.4")
public StringBasedR2dbcQuery(R2dbcQueryMethod queryMethod, R2dbcEntityOperations entityOperations,
R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
@@ -79,26 +87,60 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
* Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod},
* {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}.
*
* @param query must not be {@literal null}.
* @param method must not be {@literal null}.
* @param entityOperations must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param dataAccessStrategy must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @deprecated use the constructor version with {@link ValueExpressionDelegate}
*/
@Deprecated(since = "3.4")
public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityOperations entityOperations,
R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
this(query, method, entityOperations, converter, dataAccessStrategy, new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser)));
}
/**
* Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod},
* {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}.
*
* @param method must not be {@literal null}.
* @param entityOperations must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param dataAccessStrategy must not be {@literal null}.
* @param valueExpressionDelegate must not be {@literal null}.
*/
public StringBasedR2dbcQuery(R2dbcQueryMethod method, R2dbcEntityOperations entityOperations,
R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ValueExpressionDelegate valueExpressionDelegate) {
this(method.getRequiredAnnotatedQuery(), method, entityOperations, converter, dataAccessStrategy, valueExpressionDelegate);
}
/**
* Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod},
* {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}.
*
* @param method must not be {@literal null}.
* @param entityOperations must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param dataAccessStrategy must not be {@literal null}.
* @param valueExpressionDelegate must not be {@literal null}.
*/
public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityOperations entityOperations,
R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ValueExpressionDelegate valueExpressionDelegate) {
super(method, entityOperations, converter);
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.valueExpressionDelegate = valueExpressionDelegate;
Assert.hasText(query, "Query must not be empty");
this.dataAccessStrategy = dataAccessStrategy;
this.expressionQuery = ExpressionQuery.create(query);
this.expressionQuery = ExpressionQuery.create(valueExpressionDelegate, query);
this.binder = new ExpressionEvaluatingParameterBinder(expressionQuery, dataAccessStrategy);
this.valueContextProvider = valueExpressionDelegate.createValueContextProvider(
method.getParameters());
this.expressionDependencies = createExpressionDependencies();
if (method.isSliceQuery()) {
@@ -126,7 +168,7 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
List<ExpressionDependencies> dependencies = new ArrayList<>();
for (ExpressionQuery.ParameterBinding binding : expressionQuery.getBindings()) {
dependencies.add(ExpressionDependencies.discover(expressionParser.parseExpression(binding.getExpression())));
dependencies.add(valueExpressionDelegate.parse(binding.getExpression()).getExpressionDependencies());
}
return ExpressionDependencies.merged(dependencies);
@@ -160,11 +202,11 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
}
private Mono<R2dbcSpELExpressionEvaluator> getSpelEvaluator(RelationalParameterAccessor accessor) {
return evaluationContextProvider
.getEvaluationContextLater(getQueryMethod().getParameters(), accessor.getValues(), expressionDependencies)
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, "ValueEvaluationContextProvider must be reactive");
return ((ReactiveValueEvaluationContextProvider) valueContextProvider)
.getEvaluationContextLater(accessor.getValues(), expressionDependencies)
.<R2dbcSpELExpressionEvaluator> map(
context -> new DefaultR2dbcSpELExpressionEvaluator(expressionParser, context))
context -> new DefaultR2dbcSpELExpressionEvaluator(valueExpressionDelegate, context))
.defaultIfEmpty(DefaultR2dbcSpELExpressionEvaluator.unsupported());
}

View File

@@ -1,51 +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.support;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
/**
* Caching variant of {@link ExpressionParser}. This implementation does not support
* {@link #parseExpression(String, ParserContext) parsing with ParseContext}.
*
* @author Mark Paluch
* @since 1.2
*/
class CachingExpressionParser implements ExpressionParser {
private final ExpressionParser delegate;
private final Map<String, Expression> cache = new ConcurrentHashMap<>();
CachingExpressionParser(ExpressionParser delegate) {
this.delegate = delegate;
}
@Override
public Expression parseExpression(String expressionString) throws ParseException {
return cache.computeIfAbsent(expressionString, delegate::parseExpression);
}
@Override
public Expression parseExpression(String expressionString, ParserContext context) throws ParseException {
throw new UnsupportedOperationException("Parsing using ParserContext is not supported");
}
}

View File

@@ -37,13 +37,12 @@ import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.data.repository.query.CachingValueExpressionDelegate;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.util.Assert;
@@ -56,8 +55,6 @@ import org.springframework.util.Assert;
*/
public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final DatabaseClient databaseClient;
private final ReactiveDataAccessStrategy dataAccessStrategy;
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
@@ -116,11 +113,9 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
}
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new R2dbcQueryLookupStrategy(this.operations,
(ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider, this.converter,
this.dataAccessStrategy));
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(new R2dbcQueryLookupStrategy(operations, new CachingValueExpressionDelegate(valueExpressionDelegate), converter, dataAccessStrategy));
}
public <T, ID> RelationalEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
@@ -145,19 +140,17 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static class R2dbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
private final R2dbcEntityOperations entityOperations;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final R2dbcConverter converter;
private final ValueExpressionDelegate delegate;
private final ReactiveDataAccessStrategy dataAccessStrategy;
private final ExpressionParser parser = new CachingExpressionParser(EXPRESSION_PARSER);
R2dbcQueryLookupStrategy(R2dbcEntityOperations entityOperations,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, R2dbcConverter converter,
ValueExpressionDelegate delegate, R2dbcConverter converter,
ReactiveDataAccessStrategy dataAccessStrategy) {
super(converter.getMappingContext(), dataAccessStrategy.getDialect());
this.delegate = delegate;
this.entityOperations = entityOperations;
this.evaluationContextProvider = evaluationContextProvider;
this.converter = converter;
this.dataAccessStrategy = dataAccessStrategy;
}
@@ -175,8 +168,7 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
: queryMethod.getRequiredAnnotatedQuery();
query = evaluateTableExpressions(metadata, query);
return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter,
this.dataAccessStrategy, parser, this.evaluationContextProvider);
return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy, this.delegate);
} else {
return new PartTreeR2dbcQuery(queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy);

View File

@@ -17,9 +17,10 @@ package org.springframework.data.r2dbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.data.expression.ValueExpressionParser;
/**
* Unit tests for {@link ExpressionQuery}.
*
@@ -32,18 +33,15 @@ class ExpressionQueryUnitTests {
void bindsMultipleSpelParametersCorrectly() {
ExpressionQuery query = ExpressionQuery
.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})");
assertThat(query.getQuery())
.isEqualTo("INSERT IGNORE INTO table (x, y) VALUES (:__synthetic_0__, :__synthetic_1__)");
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(query.getBindings()).hasSize(2);
softly.assertThat(query.getBindings().get(0).getExpression()).isEqualTo("#point.x");
softly.assertThat(query.getBindings().get(0).getParameterName()).isEqualTo("__synthetic_0__");
softly.assertThat(query.getBindings().get(1).getExpression()).isEqualTo("#point.y");
softly.assertThat(query.getBindings().get(1).getParameterName()).isEqualTo("__synthetic_1__");
});
assertThat(query.getBindings()).hasSize(2);
assertThat(query.getBindings().get(0).getExpression()).isEqualTo("#{#point.x}");
assertThat(query.getBindings().get(0).getParameterName()).isEqualTo("__synthetic_0__");
assertThat(query.getBindings().get(1).getExpression()).isEqualTo("${point.y}");
assertThat(query.getBindings().get(1).getParameterName()).isEqualTo("__synthetic_1__");
}
}

View File

@@ -25,6 +25,7 @@ import reactor.test.StepVerifier;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -33,8 +34,10 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Sort;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
@@ -51,8 +54,10 @@ import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.r2dbc.core.PreparedOperation;
import org.springframework.r2dbc.core.binding.BindTarget;
@@ -67,7 +72,7 @@ import org.springframework.util.ReflectionUtils;
@MockitoSettings(strictness = Strictness.LENIENT)
public class StringBasedR2dbcQueryUnitTests {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
@Mock private R2dbcEntityOperations entityOperations;
@Mock private BindTarget bindTarget;
@@ -77,6 +82,7 @@ public class StringBasedR2dbcQueryUnitTests {
private ReactiveDataAccessStrategy accessStrategy;
private ProjectionFactory factory;
private RepositoryMetadata metadata;
private MockEnvironment environment;
@BeforeEach
void setUp() {
@@ -86,6 +92,7 @@ public class StringBasedR2dbcQueryUnitTests {
this.accessStrategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE, converter);
this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class);
this.factory = new SpelAwareProxyProjectionFactory();
this.environment = new MockEnvironment();
}
@Test
@@ -322,8 +329,10 @@ public class StringBasedR2dbcQueryUnitTests {
R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory, converter.getMappingContext());
return new StringBasedR2dbcQuery(queryMethod, entityOperations, converter, accessStrategy, PARSER,
ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(
environment, Collections.emptySet());
return new StringBasedR2dbcQuery(queryMethod, entityOperations, converter, accessStrategy, new ValueExpressionDelegate(accessor, PARSER));
}
@SuppressWarnings("unused")

View File

@@ -24,7 +24,8 @@ import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.dialect.H2Dialect;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ReactiveExtensionAwareEvaluationContextProvider;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.test.util.ReflectionTestUtils;
@@ -49,8 +50,8 @@ class R2dbcRepositoryFactoryBeanUnitTests {
Object factory = ReflectionTestUtils.getField(factoryBean, "factory");
Object evaluationContextProvider = ReflectionTestUtils.getField(factory, "evaluationContextProvider");
assertThat(evaluationContextProvider).isInstanceOf(ReactiveExtensionAwareQueryMethodEvaluationContextProvider.class)
.isNotEqualTo(ReactiveExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT);
assertThat(evaluationContextProvider).isInstanceOf(ReactiveExtensionAwareEvaluationContextProvider.class)
.isNotEqualTo(EvaluationContextProvider.DEFAULT);
}
static class Person {}