Skip to content

Commit

Permalink
Add support for Value Expressions for Repository Query methods.
Browse files Browse the repository at this point in the history
Closes #1904
Original pull request: #1906
  • Loading branch information
marcingrzejszczak authored and mp911de committed Oct 9, 2024
1 parent fd4aedc commit d526cd3
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,38 @@
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.sql.SQLType;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
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.JdbcConverter;
import org.springframework.data.jdbc.core.mapping.JdbcValue;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
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.Parameters;
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.SpelEvaluator;
import org.springframework.data.repository.query.SpelQueryContext;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
Expand Down Expand Up @@ -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 final JdbcConverter converter;
private final RowMapperFactory rowMapperFactory;
private final SpelEvaluator spelEvaluator;
private final ValueExpressionQueryRewriter.ParsedQuery parsedQuery;
private final boolean containsSpelExpressions;
private final String query;

private final CachedRowMapperFactory cachedRowMapperFactory;
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}
Expand All @@ -88,7 +97,9 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
* @param queryMethod 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).
* @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead.
*/
@Deprecated(since = "3.4")
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
@Nullable RowMapper<?> defaultRowMapper, JdbcConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Expand Down Expand Up @@ -116,6 +127,23 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
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}
* and {@link RowMapperFactory}.
Expand All @@ -125,15 +153,13 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
* @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}.
* @param delegate must not be {@literal null}.
* @since 3.4
*/
public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
RowMapperFactory rowMapperFactory, JdbcConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {

ValueExpressionDelegate delegate) {
super(queryMethod, operations);

Assert.hasText(query, "Query must not be null or empty");
Assert.notNull(rowMapperFactory, "RowMapperFactory must not be null");

Expand All @@ -160,13 +186,40 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara
this.cachedResultSetExtractorFactory = new CachedResultSetExtractorFactory(
this.cachedRowMapperFactory::getRowMapper);

SpelQueryContext.EvaluatingSpelQueryContext queryContext = SpelQueryContext
.of((counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat)
.withEvaluationContextProvider(evaluationContextProvider);
this.parameterBindings = new ArrayList<>();

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.spelEvaluator = queryContext.parse(this.query, getQueryMethod().getParameters());
this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(this.query);
this.parsedQuery = rewriter.parse(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
Expand All @@ -178,15 +231,19 @@ public Object execute(Object[] objects) {
JdbcQueryExecution<?> queryExecution = createJdbcQueryExecution(accessor, processor);
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) {

spelEvaluator.evaluate(objects).forEach(parameterMap::addValue);
return spelEvaluator.getQueryString();
ValueEvaluationContext evaluationContext = delegate.createValueContextProvider(bindableParameters)
.getEvaluationContext(objects);
for (Map.Entry<String, String> entry : parameterBindings) {
parameterMap.addValue(
entry.getKey(), delegate.parse(entry.getValue()).evaluate(evaluationContext));
}
return parsedQuery.getQueryString();
}

return this.query;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
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.ValueExpressionDelegate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
Expand Down Expand Up @@ -73,28 +73,28 @@ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
private final JdbcConverter converter;
private final QueryMappingConfiguration queryMappingConfiguration;
private final NamedParameterJdbcOperations operations;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
protected final ValueExpressionDelegate delegate;

JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueExpressionDelegate delegate) {

super(context, dialect);

Assert.notNull(publisher, "ApplicationEventPublisher must not be null");
Assert.notNull(converter, "JdbcConverter must not be null");
Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration 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.publisher = publisher;
this.callbacks = callbacks;
this.converter = converter;
this.queryMappingConfiguration = queryMappingConfiguration;
this.operations = operations;
this.evaluationContextProvider = evaluationContextProvider;
this.delegate = delegate;
}

public RelationalMappingContext getMappingContext() {
Expand All @@ -112,10 +112,10 @@ static class CreateQueryLookupStrategy extends JdbcQueryLookupStrategy {
CreateQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueExpressionDelegate delegate) {

super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations,
evaluationContextProvider);
delegate);
}

@Override
Expand Down Expand Up @@ -143,9 +143,9 @@ static class DeclaredQueryLookupStrategy extends JdbcQueryLookupStrategy {
DeclaredQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
@Nullable BeanFactory beanfactory, QueryMethodEvaluationContextProvider evaluationContextProvider) {
@Nullable BeanFactory beanfactory, ValueExpressionDelegate delegate) {
super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations,
evaluationContextProvider);
delegate);

this.rowMapperFactory = new BeanFactoryRowMapperFactory(beanfactory);
}
Expand All @@ -166,7 +166,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository
String queryString = evaluateTableExpressions(repositoryMetadata, queryMethod.getRequiredQuery());

return new StringBasedJdbcQuery(queryString, queryMethod, getOperations(), rowMapperFactory, getConverter(),
evaluationContextProvider);
delegate);
}

throw new IllegalStateException(
Expand Down Expand Up @@ -235,10 +235,10 @@ static class CreateIfNotFoundQueryLookupStrategy extends JdbcQueryLookupStrategy
RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
CreateQueryLookupStrategy createStrategy,
DeclaredQueryLookupStrategy lookupStrategy, QueryMethodEvaluationContextProvider evaluationContextProvider) {
DeclaredQueryLookupStrategy lookupStrategy, ValueExpressionDelegate delegate) {

super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations,
evaluationContextProvider);
delegate);

Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null");
Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null");
Expand Down Expand Up @@ -284,20 +284,20 @@ JdbcQueryMethod getJdbcQueryMethod(Method method, RepositoryMetadata repositoryM
public static QueryLookupStrategy create(@Nullable Key key, ApplicationEventPublisher publisher,
@Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
@Nullable BeanFactory beanFactory, QueryMethodEvaluationContextProvider evaluationContextProvider) {

@Nullable BeanFactory beanFactory, ValueExpressionDelegate delegate) {
Assert.notNull(publisher, "ApplicationEventPublisher must not be null");
Assert.notNull(context, "RelationalMappingContextPublisher must not be null");
Assert.notNull(converter, "JdbcConverter must not be null");
Assert.notNull(dialect, "Dialect must not be null");
Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration 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,
converter, dialect, queryMappingConfiguration, operations, evaluationContextProvider);
converter, dialect, queryMappingConfiguration, operations, delegate);

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;

Expand All @@ -311,7 +311,7 @@ public static QueryLookupStrategy create(@Nullable Key key, ApplicationEventPubl
case CREATE_IF_NOT_FOUND:
return new CreateIfNotFoundQueryLookupStrategy(publisher, callbacks, context, converter, dialect,
queryMappingConfiguration, operations, createQueryLookupStrategy, declaredQueryLookupStrategy,
evaluationContextProvider);
delegate);
default:
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import org.springframework.data.repository.core.support.PersistentEntityInformation;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
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.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -132,12 +132,10 @@ protected Class<?> getRepositoryBaseClass(RepositoryMetadata repositoryMetadata)
return SimpleJdbcRepository.class;
}

@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable QueryLookupStrategy.Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {

@Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrategy.Key key,
ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(JdbcQueryLookupStrategy.create(key, publisher, entityCallbacks, context, converter, dialect,
queryMappingConfiguration, operations, beanFactory, evaluationContextProvider));
queryMappingConfiguration, operations, beanFactory, valueExpressionDelegate));
}

/**
Expand Down
Loading

0 comments on commit d526cd3

Please sign in to comment.