Browse Source

Add support for Value Expressions for Repository Query methods.

Closes #1904
Original pull request: #1906
pull/1912/head
Marcin Grzejszczak 1 year ago committed by Mark Paluch
parent
commit
d526cd3a22
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 91
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java
  2. 34
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java
  3. 10
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java
  4. 31
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java
  5. 5
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java
  6. 20
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/DefaultR2dbcSpELExpressionEvaluator.java
  7. 10
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/ExpressionQuery.java
  8. 62
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java
  9. 51
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/CachingExpressionParser.java
  10. 26
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java
  11. 18
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/ExpressionQueryUnitTests.java
  12. 17
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java
  13. 7
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBeanUnitTests.java

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

@ -20,16 +20,20 @@ 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.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes;
import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.mapping.JdbcValue; import org.springframework.data.jdbc.core.mapping.JdbcValue;
@ -37,14 +41,17 @@ import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.query.CachingValueExpressionDelegate;
import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.SpelEvaluator; import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.SpelQueryContext; 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,12 +81,14 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or use the javac flag -parameters"; private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or use the javac flag -parameters";
private final JdbcConverter converter; private final JdbcConverter converter;
private final RowMapperFactory rowMapperFactory; private final RowMapperFactory rowMapperFactory;
private final SpelEvaluator spelEvaluator; private final ValueExpressionQueryRewriter.ParsedQuery parsedQuery;
private final boolean containsSpelExpressions; 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 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}
@ -88,7 +97,9 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
* @param queryMethod must not be {@literal null}. * @param queryMethod must not be {@literal null}.
* @param operations must not be {@literal null}. * @param operations must not be {@literal null}.
* @param defaultRowMapper can be {@literal null} (only in case of a modifying query). * @param defaultRowMapper can be {@literal null} (only in case of a modifying query).
* @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead.
*/ */
@Deprecated(since = "3.4")
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
@Nullable RowMapper<?> defaultRowMapper, JdbcConverter converter, @Nullable RowMapper<?> defaultRowMapper, JdbcConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider) { QueryMethodEvaluationContextProvider evaluationContextProvider) {
@ -116,6 +127,23 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
evaluationContextProvider); evaluationContextProvider);
} }
/**
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
* and {@link RowMapperFactory}.
*
* @param queryMethod must not be {@literal null}.
* @param operations must not be {@literal null}.
* @param rowMapperFactory must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @since 3.4
*/
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
RowMapperFactory rowMapperFactory, JdbcConverter converter,
ValueExpressionDelegate delegate) {
this(queryMethod.getRequiredQuery(), queryMethod, operations, rowMapperFactory, converter, delegate);
}
/** /**
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
* and {@link RowMapperFactory}. * and {@link RowMapperFactory}.
@ -125,15 +153,13 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
* @param operations must not be {@literal null}. * @param operations must not be {@literal null}.
* @param rowMapperFactory must not be {@literal null}. * @param rowMapperFactory must not be {@literal null}.
* @param converter must not be {@literal null}. * @param converter must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}. * @param delegate must not be {@literal null}.
* @since 3.4 * @since 3.4
*/ */
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) { ValueExpressionDelegate delegate) {
super(queryMethod, operations); super(queryMethod, operations);
Assert.hasText(query, "Query must not be null or empty"); Assert.hasText(query, "Query must not be null or empty");
Assert.notNull(rowMapperFactory, "RowMapperFactory must not be null"); Assert.notNull(rowMapperFactory, "RowMapperFactory must not be null");
@ -160,13 +186,40 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
this.cachedResultSetExtractorFactory = new CachedResultSetExtractorFactory( this.cachedResultSetExtractorFactory = new CachedResultSetExtractorFactory(
this.cachedRowMapperFactory::getRowMapper); this.cachedRowMapperFactory::getRowMapper);
SpelQueryContext.EvaluatingSpelQueryContext queryContext = SpelQueryContext this.parameterBindings = new ArrayList<>();
.of((counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat)
.withEvaluationContextProvider(evaluationContextProvider); 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.spelEvaluator = queryContext.parse(this.query, getQueryMethod().getParameters()); this.parsedQuery = rewriter.parse(this.query);
this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(this.query); this.containsSpelExpressions = !this.parsedQuery.getQueryString().equals(this.query);
this.delegate = delegate;
}
/**
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
* and {@link RowMapperFactory}.
*
* @param query must not be {@literal null} or empty.
* @param queryMethod must not be {@literal null}.
* @param operations must not be {@literal null}.
* @param rowMapperFactory must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @since 3.4
* @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead.
*/
@Deprecated(since = "3.4")
public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
RowMapperFactory rowMapperFactory, JdbcConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(query, queryMethod, operations, rowMapperFactory, converter, new CachingValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(null,
rootObject -> evaluationContextProvider.getEvaluationContext(queryMethod.getParameters(), new Object[] { rootObject })), ValueExpressionParser.create(
SpelExpressionParser::new)));
} }
@Override @Override
@ -178,15 +231,19 @@ 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, parameterMap), parameterMap); return queryExecution.execute(processSpelExpressions(objects, accessor.getBindableParameters(), parameterMap), parameterMap);
} }
private String processSpelExpressions(Object[] objects, MapSqlParameterSource parameterMap) { private String processSpelExpressions(Object[] objects, Parameters<?, ?> bindableParameters, MapSqlParameterSource parameterMap) {
if (containsSpelExpressions) { if (containsSpelExpressions) {
ValueEvaluationContext evaluationContext = delegate.createValueContextProvider(bindableParameters)
spelEvaluator.evaluate(objects).forEach(parameterMap::addValue); .getEvaluationContext(objects);
return spelEvaluator.getQueryString(); for (Map.Entry<String, String> entry : parameterBindings) {
parameterMap.addValue(
entry.getKey(), delegate.parse(entry.getValue()).evaluate(evaluationContext));
}
return parsedQuery.getQueryString();
} }
return this.query; return this.query;

34
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java

@ -41,8 +41,8 @@ import org.springframework.data.relational.repository.support.RelationalQueryLoo
import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
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.SingleColumnRowMapper; import org.springframework.jdbc.core.SingleColumnRowMapper;
@ -73,12 +73,12 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
private final JdbcConverter converter; private final JdbcConverter converter;
private final QueryMappingConfiguration queryMappingConfiguration; private final QueryMappingConfiguration queryMappingConfiguration;
private final NamedParameterJdbcOperations operations; private final NamedParameterJdbcOperations operations;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider; protected final ValueExpressionDelegate delegate;
JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
RelationalMappingContext context, JdbcConverter converter, Dialect dialect, RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider) { ValueExpressionDelegate delegate) {
super(context, dialect); super(context, dialect);
@ -86,7 +86,7 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
Assert.notNull(converter, "JdbcConverter must not be null"); Assert.notNull(converter, "JdbcConverter must not be null");
Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null"); Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null");
Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); Assert.notNull(operations, "NamedParameterJdbcOperations must not be null");
Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null"); Assert.notNull(delegate, "ValueExpressionDelegate must not be null");
this.context = context; this.context = context;
this.publisher = publisher; this.publisher = publisher;
@ -94,7 +94,7 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
this.converter = converter; this.converter = converter;
this.queryMappingConfiguration = queryMappingConfiguration; this.queryMappingConfiguration = queryMappingConfiguration;
this.operations = operations; this.operations = operations;
this.evaluationContextProvider = evaluationContextProvider; this.delegate = delegate;
} }
public RelationalMappingContext getMappingContext() { public RelationalMappingContext getMappingContext() {
@ -112,10 +112,10 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
CreateQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, CreateQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
RelationalMappingContext context, JdbcConverter converter, Dialect dialect, RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider) { ValueExpressionDelegate delegate) {
super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations,
evaluationContextProvider); delegate);
} }
@Override @Override
@ -143,9 +143,9 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
DeclaredQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, DeclaredQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
RelationalMappingContext context, JdbcConverter converter, Dialect dialect, RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
@Nullable BeanFactory beanfactory, QueryMethodEvaluationContextProvider evaluationContextProvider) { @Nullable BeanFactory beanfactory, ValueExpressionDelegate delegate) {
super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations,
evaluationContextProvider); delegate);
this.rowMapperFactory = new BeanFactoryRowMapperFactory(beanfactory); this.rowMapperFactory = new BeanFactoryRowMapperFactory(beanfactory);
} }
@ -166,7 +166,7 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
String queryString = evaluateTableExpressions(repositoryMetadata, queryMethod.getRequiredQuery()); String queryString = evaluateTableExpressions(repositoryMetadata, queryMethod.getRequiredQuery());
return new StringBasedJdbcQuery(queryString, queryMethod, getOperations(), rowMapperFactory, getConverter(), return new StringBasedJdbcQuery(queryString, queryMethod, getOperations(), rowMapperFactory, getConverter(),
evaluationContextProvider); delegate);
} }
throw new IllegalStateException( throw new IllegalStateException(
@ -235,10 +235,10 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
RelationalMappingContext context, JdbcConverter converter, Dialect dialect, RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
CreateQueryLookupStrategy createStrategy, CreateQueryLookupStrategy createStrategy,
DeclaredQueryLookupStrategy lookupStrategy, QueryMethodEvaluationContextProvider evaluationContextProvider) { DeclaredQueryLookupStrategy lookupStrategy, ValueExpressionDelegate delegate) {
super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations,
evaluationContextProvider); delegate);
Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null"); Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null");
Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null"); Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null");
@ -284,20 +284,20 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
public static QueryLookupStrategy create(@Nullable Key key, ApplicationEventPublisher publisher, public static QueryLookupStrategy create(@Nullable Key key, ApplicationEventPublisher publisher,
@Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
@Nullable BeanFactory beanFactory, QueryMethodEvaluationContextProvider evaluationContextProvider) { @Nullable BeanFactory beanFactory, ValueExpressionDelegate delegate) {
Assert.notNull(publisher, "ApplicationEventPublisher must not be null"); Assert.notNull(publisher, "ApplicationEventPublisher must not be null");
Assert.notNull(context, "RelationalMappingContextPublisher must not be null"); Assert.notNull(context, "RelationalMappingContextPublisher must not be null");
Assert.notNull(converter, "JdbcConverter must not be null"); Assert.notNull(converter, "JdbcConverter must not be null");
Assert.notNull(dialect, "Dialect must not be null"); Assert.notNull(dialect, "Dialect must not be null");
Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null"); Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null");
Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); Assert.notNull(operations, "NamedParameterJdbcOperations must not be null");
Assert.notNull(delegate, "ValueExpressionDelegate must not be null");
CreateQueryLookupStrategy createQueryLookupStrategy = new CreateQueryLookupStrategy(publisher, callbacks, context, CreateQueryLookupStrategy createQueryLookupStrategy = new CreateQueryLookupStrategy(publisher, callbacks, context,
converter, dialect, queryMappingConfiguration, operations, evaluationContextProvider); converter, dialect, queryMappingConfiguration, operations, delegate);
DeclaredQueryLookupStrategy declaredQueryLookupStrategy = new DeclaredQueryLookupStrategy(publisher, callbacks, DeclaredQueryLookupStrategy declaredQueryLookupStrategy = new DeclaredQueryLookupStrategy(publisher, callbacks,
context, converter, dialect, queryMappingConfiguration, operations, beanFactory, evaluationContextProvider); context, converter, dialect, queryMappingConfiguration, operations, beanFactory, delegate);
Key keyToUse = key != null ? key : Key.CREATE_IF_NOT_FOUND; Key keyToUse = key != null ? key : Key.CREATE_IF_NOT_FOUND;
@ -311,7 +311,7 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
case CREATE_IF_NOT_FOUND: case CREATE_IF_NOT_FOUND:
return new CreateIfNotFoundQueryLookupStrategy(publisher, callbacks, context, converter, dialect, return new CreateIfNotFoundQueryLookupStrategy(publisher, callbacks, context, converter, dialect,
queryMappingConfiguration, operations, createQueryLookupStrategy, declaredQueryLookupStrategy, queryMappingConfiguration, operations, createQueryLookupStrategy, declaredQueryLookupStrategy,
evaluationContextProvider); delegate);
default: default:
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key)); throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key));
} }

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

@ -33,7 +33,7 @@ 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.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -132,12 +132,10 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
return SimpleJdbcRepository.class; return SimpleJdbcRepository.class;
} }
@Override @Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrategy.Key key,
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable QueryLookupStrategy.Key key, ValueExpressionDelegate valueExpressionDelegate) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
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, evaluationContextProvider)); queryMappingConfiguration, operations, beanFactory, valueExpressionDelegate));
} }
/** /**

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

@ -33,7 +33,10 @@ import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach; 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.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter; import org.springframework.data.convert.WritingConverter;
@ -41,6 +44,7 @@ import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.core.convert.JdbcTypeFactory; import org.springframework.data.jdbc.core.convert.JdbcTypeFactory;
@ -53,9 +57,9 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
@ -82,7 +86,7 @@ class StringBasedJdbcQueryUnitTests {
NamedParameterJdbcOperations operations; NamedParameterJdbcOperations operations;
RelationalMappingContext context; RelationalMappingContext context;
JdbcConverter converter; JdbcConverter converter;
QueryMethodEvaluationContextProvider evaluationContextProvider; ValueExpressionDelegate delegate;
@BeforeEach @BeforeEach
void setup() { void setup() {
@ -91,7 +95,8 @@ 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));
this.evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), new StaticApplicationContext());
this.delegate = new ValueExpressionDelegate(accessor, ValueExpressionParser.create());
} }
@Test // DATAJDBC-165 @Test // DATAJDBC-165
@ -248,7 +253,7 @@ class StringBasedJdbcQueryUnitTests {
JdbcQueryMethod queryMethod = createMethod("sliceAll", Pageable.class); JdbcQueryMethod queryMethod = createMethod("sliceAll", Pageable.class);
assertThatThrownBy( assertThatThrownBy(
() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider)) () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate))
.isInstanceOf(UnsupportedOperationException.class) .isInstanceOf(UnsupportedOperationException.class)
.hasMessageContaining("Slice queries are not supported using string-based queries"); .hasMessageContaining("Slice queries are not supported using string-based queries");
} }
@ -259,7 +264,7 @@ class StringBasedJdbcQueryUnitTests {
JdbcQueryMethod queryMethod = createMethod("pageAll", Pageable.class); JdbcQueryMethod queryMethod = createMethod("pageAll", Pageable.class);
assertThatThrownBy( assertThatThrownBy(
() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider)) () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate))
.isInstanceOf(UnsupportedOperationException.class) .isInstanceOf(UnsupportedOperationException.class)
.hasMessageContaining("Page queries are not supported using string-based queries"); .hasMessageContaining("Page queries are not supported using string-based queries");
} }
@ -270,7 +275,7 @@ class StringBasedJdbcQueryUnitTests {
JdbcQueryMethod queryMethod = createMethod("unsupportedLimitQuery", String.class, Limit.class); JdbcQueryMethod queryMethod = createMethod("unsupportedLimitQuery", String.class, Limit.class);
assertThatThrownBy( assertThatThrownBy(
() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider)) () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate))
.isInstanceOf(UnsupportedOperationException.class); .isInstanceOf(UnsupportedOperationException.class);
} }
@ -350,11 +355,11 @@ class StringBasedJdbcQueryUnitTests {
List<EvaluationContextExtension> list = new ArrayList<>(); List<EvaluationContextExtension> list = new ArrayList<>();
list.add(new MyEvaluationContextProvider()); list.add(new MyEvaluationContextProvider());
QueryMethodEvaluationContextProvider evaluationContextProviderImpl = new ExtensionAwareQueryMethodEvaluationContextProvider(
list);
StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), list);
evaluationContextProviderImpl); this.delegate = new ValueExpressionDelegate(accessor, ValueExpressionParser.create());
StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate);
ArgumentCaptor<SqlParameterSource> paramSource = ArgumentCaptor.forClass(SqlParameterSource.class); ArgumentCaptor<SqlParameterSource> paramSource = ArgumentCaptor.forClass(SqlParameterSource.class);
ArgumentCaptor<String> query = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> query = ArgumentCaptor.forClass(String.class);
@ -398,7 +403,7 @@ class StringBasedJdbcQueryUnitTests {
: this.converter; : this.converter;
StringBasedJdbcQuery query = new StringBasedJdbcQuery(method.getDeclaredQuery(), method, operations, result -> mock(RowMapper.class), StringBasedJdbcQuery query = new StringBasedJdbcQuery(method.getDeclaredQuery(), method, operations, result -> mock(RowMapper.class),
converter, evaluationContextProvider); converter, delegate);
query.execute(arguments); query.execute(arguments);
@ -434,7 +439,7 @@ class StringBasedJdbcQueryUnitTests {
} }
private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod, String preparedReference, Object value) { private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod, String preparedReference, Object value) {
return new StringBasedJdbcQuery(queryMethod, operations, new StubRowMapperFactory(preparedReference, value), converter, evaluationContextProvider); return new StringBasedJdbcQuery(queryMethod, operations, new StubRowMapperFactory(preparedReference, value), converter, delegate);
} }
interface MyRepository extends Repository<Object, Long> { interface MyRepository extends Repository<Object, Long> {

5
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java

@ -42,6 +42,7 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@ -137,7 +138,7 @@ class JdbcQueryLookupStrategyUnitTests {
.registerRowMapper(NumberFormat.class, numberFormatMapper); .registerRowMapper(NumberFormat.class, numberFormatMapper);
QueryLookupStrategy queryLookupStrategy = JdbcQueryLookupStrategy.create(key, publisher, callbacks, mappingContext, QueryLookupStrategy queryLookupStrategy = JdbcQueryLookupStrategy.create(key, publisher, callbacks, mappingContext,
converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null, evaluationContextProvider); converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null, ValueExpressionDelegate.create());
assertThat(queryLookupStrategy).isInstanceOf(expectedClass); assertThat(queryLookupStrategy).isInstanceOf(expectedClass);
} }
@ -157,7 +158,7 @@ class JdbcQueryLookupStrategyUnitTests {
QueryMappingConfiguration mappingConfiguration) { QueryMappingConfiguration mappingConfiguration) {
QueryLookupStrategy queryLookupStrategy = JdbcQueryLookupStrategy.create(key, publisher, callbacks, mappingContext, QueryLookupStrategy queryLookupStrategy = JdbcQueryLookupStrategy.create(key, publisher, callbacks, mappingContext,
converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null, evaluationContextProvider); converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null, ValueExpressionDelegate.create());
Method method = ReflectionUtils.findMethod(MyRepository.class, name); Method method = ReflectionUtils.findMethod(MyRepository.class, name);
return queryLookupStrategy.resolveQuery(method, metadata, projectionFactory, namedQueries); return queryLookupStrategy.resolveQuery(method, metadata, projectionFactory, namedQueries);

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

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

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

@ -18,7 +18,8 @@ package org.springframework.data.r2dbc.repository.query;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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 * 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. * @param query the query string to parse.
* @return the parsed {@link ExpressionQuery}. * @return the parsed {@link ExpressionQuery}.
*/ */
public static ExpressionQuery create(String query) { public static ExpressionQuery create(ValueExpressionParser parser, String query) {
List<ParameterBinding> parameterBindings = new ArrayList<>(); 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); String parameterName = String.format(SYNTHETIC_PARAMETER_TEMPLATE, counter);
parameterBindings.add(new ParameterBinding(parameterName, expression)); parameterBindings.add(new ParameterBinding(parameterName, expression));
return parameterName; return parameterName;
}, String::concat); }, String::concat);
SpelQueryContext.SpelExtractor parsed = queryContext.parse(query); ValueExpressionQueryRewriter.ParsedQuery parsed = rewriter.parse(query);
return new ExpressionQuery(parsed.getQueryString(), parameterBindings); return new ExpressionQuery(parsed.getQueryString(), parameterBindings);
} }

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

@ -22,6 +22,10 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.convert.R2dbcConverter;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations; import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; 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.r2dbc.repository.Query;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; 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.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.spel.ExpressionDependencies; import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
@ -52,10 +58,10 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
private final ExpressionQuery expressionQuery; private final ExpressionQuery expressionQuery;
private final ExpressionEvaluatingParameterBinder binder; private final ExpressionEvaluatingParameterBinder binder;
private final ExpressionParser expressionParser;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final ExpressionDependencies expressionDependencies; private final ExpressionDependencies expressionDependencies;
private final ReactiveDataAccessStrategy dataAccessStrategy; private final ReactiveDataAccessStrategy dataAccessStrategy;
private final ValueExpressionDelegate valueExpressionDelegate;
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},
@ -67,7 +73,9 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
* @param dataAccessStrategy must not be {@literal null}. * @param dataAccessStrategy must not be {@literal null}.
* @param expressionParser must not be {@literal null}. * @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider 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, public StringBasedR2dbcQuery(R2dbcQueryMethod queryMethod, R2dbcEntityOperations entityOperations,
R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser, R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
@ -79,26 +87,60 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
* Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod},
* {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}.
* *
* @param query must not be {@literal null}.
* @param method must not be {@literal null}. * @param method must not be {@literal null}.
* @param entityOperations must not be {@literal null}. * @param entityOperations must not be {@literal null}.
* @param converter must not be {@literal null}. * @param converter must not be {@literal null}.
* @param dataAccessStrategy must not be {@literal null}. * @param dataAccessStrategy must not be {@literal null}.
* @param expressionParser must not be {@literal null}. * @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider 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, public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityOperations entityOperations,
R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser, R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { 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); super(method, entityOperations, converter);
this.expressionParser = expressionParser; this.valueExpressionDelegate = valueExpressionDelegate;
this.evaluationContextProvider = evaluationContextProvider;
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(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());
this.expressionDependencies = createExpressionDependencies(); this.expressionDependencies = createExpressionDependencies();
if (method.isSliceQuery()) { if (method.isSliceQuery()) {
@ -126,7 +168,7 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
List<ExpressionDependencies> dependencies = new ArrayList<>(); List<ExpressionDependencies> dependencies = new ArrayList<>();
for (ExpressionQuery.ParameterBinding binding : expressionQuery.getBindings()) { 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); return ExpressionDependencies.merged(dependencies);
@ -160,11 +202,11 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
} }
private Mono<R2dbcSpELExpressionEvaluator> getSpelEvaluator(RelationalParameterAccessor accessor) { private Mono<R2dbcSpELExpressionEvaluator> getSpelEvaluator(RelationalParameterAccessor accessor) {
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, "ValueEvaluationContextProvider must be reactive");
return evaluationContextProvider return ((ReactiveValueEvaluationContextProvider) valueContextProvider)
.getEvaluationContextLater(getQueryMethod().getParameters(), accessor.getValues(), expressionDependencies) .getEvaluationContextLater(accessor.getValues(), expressionDependencies)
.<R2dbcSpELExpressionEvaluator> map( .<R2dbcSpELExpressionEvaluator> map(
context -> new DefaultR2dbcSpELExpressionEvaluator(expressionParser, context)) context -> new DefaultR2dbcSpELExpressionEvaluator(valueExpressionDelegate, context))
.defaultIfEmpty(DefaultR2dbcSpELExpressionEvaluator.unsupported()); .defaultIfEmpty(DefaultR2dbcSpELExpressionEvaluator.unsupported());
} }

51
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/CachingExpressionParser.java

@ -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");
}
}

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

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

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

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

17
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java

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

7
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBeanUnitTests.java

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

Loading…
Cancel
Save