Add support for Value Expressions.

Closes #3619
Original pull request: #3627
This commit is contained in:
Mark Paluch
2024-09-27 11:32:50 +02:00
parent 61e6d36c7f
commit f045352049
23 changed files with 249 additions and 187 deletions

View File

@@ -22,12 +22,12 @@ import java.util.Objects;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.Lazy;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentLruCache;
@@ -50,12 +50,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
private final DeclaredQuery query;
private final Lazy<DeclaredQuery> countQuery;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final SpelExpressionParser parser;
private final ValueExpressionDelegate valueExpressionDelegate;
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
private final QueryRewriter queryRewriter;
private final QuerySortRewriter querySortRewriter;
private final Lazy<ParameterBinder> countParameterBinder;
private final ValueEvaluationContextProvider valueExpressionContextProvider;
/**
* Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
@@ -65,30 +65,29 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null}.
* @param countQueryString must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @param parser must not be {@literal null}.
* @param queryRewriter must not be {@literal null}.
* @param valueExpressionDelegate must not be {@literal null}.
*/
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
@Nullable String countQueryString, QueryRewriter queryRewriter,
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
ValueExpressionDelegate valueExpressionDelegate) {
super(method, em);
Assert.hasText(queryString, "Query string must not be null or empty");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
Assert.notNull(parser, "Parser must not be null");
Assert.notNull(valueExpressionDelegate, "ValueExpressionDelegate must not be null");
Assert.notNull(queryRewriter, "QueryRewriter must not be null");
this.evaluationContextProvider = evaluationContextProvider;
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser,
this.valueExpressionDelegate = valueExpressionDelegate;
this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters());
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), valueExpressionDelegate,
method.isNativeQuery());
this.countQuery = Lazy.of(() -> {
if (StringUtils.hasText(countQueryString)) {
return new ExpressionBasedStringQuery(countQueryString, method.getEntityInformation(), parser,
return new ExpressionBasedStringQuery(countQueryString, method.getEntityInformation(), valueExpressionDelegate,
method.isNativeQuery());
}
@@ -99,7 +98,6 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
return this.createBinder(this.countQuery.get());
});
this.parser = parser;
this.queryRewriter = queryRewriter;
JpaParameters parameters = method.getParameters();
@@ -140,8 +138,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
}
protected ParameterBinder createBinder(DeclaredQuery query) {
return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, parser,
evaluationContextProvider);
return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query,
valueExpressionDelegate, valueExpressionContextProvider);
}
@Override

View File

@@ -15,12 +15,13 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.Objects;
import java.util.regex.Pattern;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.repository.core.EntityMetadata;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
@@ -59,7 +60,7 @@ class ExpressionBasedStringQuery extends StringQuery {
* @param parser must not be {@literal null}.
* @param nativeQuery is a given query is native or not
*/
public ExpressionBasedStringQuery(String query, JpaEntityMetadata<?> metadata, SpelExpressionParser parser,
public ExpressionBasedStringQuery(String query, JpaEntityMetadata<?> metadata, ValueExpressionParser parser,
boolean nativeQuery) {
super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query));
}
@@ -74,7 +75,7 @@ class ExpressionBasedStringQuery extends StringQuery {
* @return A query supporting SpEL expressions.
*/
static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata<?> metadata,
SpelExpressionParser parser, boolean nativeQuery) {
ValueExpressionParser parser, boolean nativeQuery) {
return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery);
}
@@ -84,7 +85,7 @@ class ExpressionBasedStringQuery extends StringQuery {
* @param parser Must not be {@literal null}.
*/
private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEntityMetadata<?> metadata,
SpelExpressionParser parser) {
ValueExpressionParser parser) {
Assert.notNull(query, "query must not be null");
Assert.notNull(metadata, "metadata must not be null");
@@ -99,9 +100,9 @@ class ExpressionBasedStringQuery extends StringQuery {
query = potentiallyQuoteExpressionsParameter(query);
Expression expr = parser.parseExpression(query, ParserContext.TEMPLATE_EXPRESSION);
ValueExpression expr = parser.parse(query);
String result = expr.getValue(evalContext, String.class);
String result = Objects.toString(expr.evaluate(ValueEvaluationContext.of(null, evalContext)));
if (result == null) {
return query;

View File

@@ -19,9 +19,8 @@ import jakarta.persistence.EntityManager;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
/**
@@ -34,31 +33,20 @@ enum JpaQueryFactory {
INSTANCE;
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
/**
* Creates a {@link RepositoryQuery} from the given {@link String} query.
*
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
* @param countQueryString
* @param queryString must not be {@literal null}.
* @param evaluationContextProvider
* @return
*/
AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString,
@Nullable String countQueryString, QueryRewriter queryRewriter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueExpressionDelegate valueExpressionDelegate) {
if (method.isScrollQuery()) {
throw QueryCreationException.create(method, "Scroll queries are not supported using String-based queries");
}
return method.isNativeQuery()
? new NativeJpaQuery(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider,
PARSER)
: new SimpleJpaQuery(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider,
PARSER);
? new NativeJpaQuery(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate)
: new SimpleJpaQuery(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate);
}
/**

View File

@@ -21,6 +21,8 @@ import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.ProjectionFactory;
@@ -30,7 +32,9 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -135,7 +139,7 @@ public final class JpaQueryLookupStrategy {
*/
private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy {
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ValueExpressionDelegate valueExpressionDelegate;
/**
* Creates a new {@link DeclaredQueryLookupStrategy}.
@@ -145,11 +149,11 @@ public final class JpaQueryLookupStrategy {
* @param evaluationContextProvider must not be {@literal null}.
*/
public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
QueryMethodEvaluationContextProvider evaluationContextProvider, QueryRewriterProvider queryRewriterProvider) {
ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider) {
super(em, queryMethodFactory, queryRewriterProvider);
this.evaluationContextProvider = evaluationContextProvider;
this.valueExpressionDelegate = delegate;
}
@Override
@@ -168,13 +172,13 @@ public final class JpaQueryLookupStrategy {
}
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getRequiredAnnotatedQuery(),
getCountQuery(method, namedQueries, em), queryRewriter, evaluationContextProvider);
getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate);
}
String name = method.getNamedQueryName();
if (namedQueries.hasQuery(name)) {
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name),
getCountQuery(method, namedQueries, em), queryRewriter, evaluationContextProvider);
getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate);
}
RepositoryQuery query = NamedQuery.lookupFrom(method, em);
@@ -267,28 +271,47 @@ public final class JpaQueryLookupStrategy {
* @param key may be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @param escape must not be {@literal null}.
* @deprecated since 3.4, use
* {@link #create(EntityManager, JpaQueryMethodFactory, Key, ValueExpressionDelegate, QueryRewriterProvider, EscapeCharacter)}
* instead.
*/
@Deprecated(since = "3.4")
public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
@Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider,
QueryRewriterProvider queryRewriterProvider, EscapeCharacter escape) {
return create(em, queryMethodFactory, key,
new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(),
evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionDelegate.create()),
queryRewriterProvider, escape);
}
/**
* Creates a {@link QueryLookupStrategy} for the given {@link EntityManager} and {@link Key}.
*
* @param em must not be {@literal null}.
* @param queryMethodFactory must not be {@literal null}.
* @param key may be {@literal null}.
* @param delegate must not be {@literal null}.
* @param queryRewriterProvider must not be {@literal null}.
* @param escape must not be {@literal null}.
*/
public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
@Nullable Key key, ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider,
EscapeCharacter escape) {
Assert.notNull(em, "EntityManager must not be null");
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null");
Assert.notNull(delegate, "ValueExpressionDelegate must not be null");
switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) {
case CREATE:
return new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape);
case USE_DECLARED_QUERY:
return new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider,
queryRewriterProvider);
case CREATE_IF_NOT_FOUND:
return new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory,
new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape),
new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider, queryRewriterProvider),
queryRewriterProvider);
default:
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key));
}
return switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) {
case CREATE -> new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape);
case USE_DECLARED_QUERY ->
new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider);
case CREATE_IF_NOT_FOUND -> new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory,
new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape),
new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider),
queryRewriterProvider);
default -> throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key));
};
}
/**

View File

@@ -25,10 +25,9 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.NativeQuery;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
@@ -57,12 +56,12 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
* @param queryString must not be {@literal null} or empty.
* @param countQueryString must not be {@literal null} or empty.
* @param rewriter the query rewriter to use.
* @param valueExpressionDelegate must not be {@literal null}.
*/
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
QueryRewriter rewriter, QueryMethodEvaluationContextProvider evaluationContextProvider,
SpelExpressionParser parser) {
QueryRewriter rewriter, ValueExpressionDelegate valueExpressionDelegate) {
super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser);
super(method, em, queryString, countQueryString, rewriter, valueExpressionDelegate);
MergedAnnotations annotations = MergedAnnotations.from(method.getMethod());
MergedAnnotation<NativeQuery> annotation = annotations.get(NativeQuery.class);

View File

@@ -18,12 +18,12 @@ package org.springframework.data.jpa.repository.query;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
/**
@@ -86,7 +86,7 @@ class ParameterBinderFactory {
* {@link jakarta.persistence.Query} while processing SpEL expressions where applicable.
*/
static ParameterBinder createQueryAwareBinder(JpaParameters parameters, DeclaredQuery query,
SpelExpressionParser parser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueExpressionParser parser, ValueEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(parameters, "JpaParameters must not be null");
Assert.notNull(query, "StringQuery must not be null");
@@ -95,7 +95,7 @@ class ParameterBinderFactory {
List<ParameterBinding> bindings = query.getParameterBindings();
QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser,
evaluationContextProvider, parameters);
evaluationContextProvider);
QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters);

View File

@@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.lang.Nullable;
@@ -493,12 +494,12 @@ class ParameterBinding {
sealed interface ParameterOrigin permits Expression,MethodInvocationArgument {
/**
* Creates a {@link Expression} for the given {@code expression} string.
* Creates a {@link Expression} for the given {@code expression}.
*
* @param expression must not be {@literal null}.
* @return {@link Expression} for the given {@code expression} string.
* @return {@link Expression} for the given {@code expression}.
*/
static Expression ofExpression(String expression) {
static Expression ofExpression(ValueExpression expression) {
return new Expression(expression);
}
@@ -562,7 +563,7 @@ class ParameterBinding {
* @author Mark Paluch
* @since 3.1.2
*/
public record Expression(String expression) implements ParameterOrigin {
public record Expression(ValueExpression expression) implements ParameterOrigin {
@Override
public boolean isMethodArgument() {

View File

@@ -21,6 +21,10 @@ import jakarta.persistence.TemporalType;
import java.util.List;
import java.util.function.Function;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument;
@@ -28,9 +32,7 @@ import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.P
import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter;
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.spel.EvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
@@ -89,14 +91,13 @@ abstract class QueryParameterSetterFactory {
* @return a {@link QueryParameterSetterFactory} that can handle
* {@link org.springframework.expression.spel.standard.SpelExpression}s.
*/
static QueryParameterSetterFactory parsing(SpelExpressionParser parser,
QueryMethodEvaluationContextProvider evaluationContextProvider, Parameters<?, ?> parameters) {
static QueryParameterSetterFactory parsing(ValueExpressionParser parser,
ValueEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(parser, "SpelExpressionParser must not be null");
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null");
Assert.notNull(parameters, "Parameters must not be null");
Assert.notNull(parser, "ValueExpressionParser must not be null");
Assert.notNull(evaluationContextProvider, "ValueEvaluationContextProvider must not be null");
return new ExpressionBasedQueryParameterSetterFactory(parser, evaluationContextProvider, parameters);
return new ExpressionBasedQueryParameterSetterFactory(parser, evaluationContextProvider);
}
/**
@@ -161,25 +162,22 @@ abstract class QueryParameterSetterFactory {
*/
private static class ExpressionBasedQueryParameterSetterFactory extends QueryParameterSetterFactory {
private final SpelExpressionParser parser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final Parameters<?, ?> parameters;
private final ValueExpressionParser parser;
private final ValueEvaluationContextProvider evaluationContextProvider;
/**
* @param parser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @param parameters must not be {@literal null}.
*/
ExpressionBasedQueryParameterSetterFactory(SpelExpressionParser parser,
QueryMethodEvaluationContextProvider evaluationContextProvider, Parameters<?, ?> parameters) {
ExpressionBasedQueryParameterSetterFactory(ValueExpressionParser parser,
ValueEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null");
Assert.notNull(parser, "SpelExpressionParser must not be null");
Assert.notNull(parameters, "Parameters must not be null");
Assert.notNull(parser, "ValueExpressionParser must not be null");
Assert.notNull(evaluationContextProvider, "ValueEvaluationContextProvider must not be null");
this.evaluationContextProvider = evaluationContextProvider;
this.parser = parser;
this.parameters = parameters;
this.evaluationContextProvider = evaluationContextProvider;
}
@Nullable
@@ -190,9 +188,7 @@ abstract class QueryParameterSetterFactory {
return null;
}
Expression expression = parser.parseExpression(e.expression());
return createSetter(values -> evaluateExpression(expression, values), binding, null);
return createSetter(values -> evaluateExpression(e.expression(), values), binding, null);
}
/**
@@ -203,11 +199,10 @@ abstract class QueryParameterSetterFactory {
* @return the result of the evaluation.
*/
@Nullable
private Object evaluateExpression(Expression expression, JpaParametersParameterAccessor accessor) {
private Object evaluateExpression(ValueExpression expression, JpaParametersParameterAccessor accessor) {
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameters, accessor.getValues());
return expression.getValue(context, Object.class);
ValueEvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(accessor.getValues());
return expression.evaluate(evaluationContext);
}
}

View File

@@ -19,9 +19,8 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
/**
@@ -43,12 +42,11 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
* @param em must not be {@literal null}
* @param countQueryString
* @param queryRewriter must not be {@literal null}
* @param evaluationContextProvider must not be {@literal null}
* @param parser must not be {@literal null}
* @param valueExpressionDelegate must not be {@literal null}
*/
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString,
QueryRewriter queryRewriter, QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryRewriter, evaluationContextProvider, parser);
QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) {
this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryRewriter, valueExpressionDelegate);
}
/**
@@ -59,13 +57,12 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
* @param queryString must not be {@literal null} or empty
* @param countQueryString
* @param queryRewriter
* @param evaluationContextProvider must not be {@literal null}
* @param parser must not be {@literal null}
* @param valueExpressionDelegate must not be {@literal null}
*/
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, QueryRewriter queryRewriter,
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
ValueExpressionDelegate valueExpressionDelegate) {
super(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider, parser);
super(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate);
validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s", method);

View File

@@ -26,13 +26,14 @@ import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
import org.springframework.data.jpa.repository.query.ParameterBinding.InParameterBinding;
import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding;
import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument;
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
import org.springframework.data.repository.query.SpelQueryContext;
import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -241,14 +242,15 @@ class StringQuery implements DeclaredQuery {
greatestParameterIndex = 0;
}
SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIndex,
ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query,
parametersShouldBeAccessedByIndex,
greatestParameterIndex);
String resultingQuery = spelExtractor.getQueryString();
String resultingQuery = parsedQuery.getQueryString();
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
int syntheticParameterIndex = expressionParameterIndex + spelExtractor.size();
int syntheticParameterIndex = expressionParameterIndex + parsedQuery.size();
ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings),
syntheticParameterIndex);
@@ -258,7 +260,7 @@ class StringQuery implements DeclaredQuery {
while (matcher.find()) {
if (spelExtractor.isQuoted(matcher.start())) {
if (parsedQuery.isQuoted(matcher.start())) {
continue;
}
@@ -282,7 +284,8 @@ class StringQuery implements DeclaredQuery {
String typeSource = matcher.group(COMPARISION_TYPE_GROUP);
Assert.isTrue(parameterIndexString != null || parameterName != null,
() -> String.format("We need either a name or an index; Offending query string: %s", query));
String expression = spelExtractor.getParameter(parameterName == null ? parameterIndexString : parameterName);
ValueExpression expression = parsedQuery
.getParameter(parameterName == null ? parameterIndexString : parameterName);
String replacement = null;
expressionParameterIndex++;
@@ -346,7 +349,8 @@ class StringQuery implements DeclaredQuery {
return resultingQuery;
}
private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean parametersShouldBeAccessedByIndex,
private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel,
boolean parametersShouldBeAccessedByIndex,
int greatestParameterIndex) {
/*
@@ -362,8 +366,10 @@ class StringQuery implements DeclaredQuery {
String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;
ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(ValueExpressionParser.create(),
indexToParameterName, parameterNameToReplacement);
return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
return rewriter.parse(queryWithSpel);
}
@Nullable

View File

@@ -56,10 +56,11 @@ import org.springframework.data.repository.core.support.QueryCreationListener;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor;
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.ReturnedType;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
@@ -236,12 +237,13 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, evaluationContextProvider,
ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key,
new CachingValueExpressionDelegate(valueExpressionDelegate),
queryRewriterProvider, escapeCharacter));
}
@Override
@SuppressWarnings("unchecked")
public <T, ID> JpaEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {

View File

@@ -309,6 +309,20 @@ class UserRepositoryFinderTests {
assertThat(userRepository.findContainingEscaped("att\\_")).containsExactly(withEscapedWildcard);
}
@Test // GH-3619
void propertyPlaceholderInQuery() {
User extra = new User("extra", "Matt_ew", "extra");
userRepository.save(extra);
System.setProperty("query.lastname", "%_ew");
assertThat(userRepository.findWithPropertyPlaceholder()).containsOnly(extra);
System.clearProperty("query.lastname");
assertThat(userRepository.findWithPropertyPlaceholder()).isEmpty();
}
@Test // DATAJPA-829
void translatesContainsToMemberOf() {

View File

@@ -26,6 +26,7 @@ import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
@@ -37,8 +38,7 @@ import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -67,7 +67,7 @@ class AbstractStringBasedJpaQueryIntegrationTests {
JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class);
AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, null, QueryRewriter.IdentityQueryRewriter.INSTANCE,
QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser());
ValueExpressionDelegate.create());
jpaQuery.createJpaQuery(method.getAnnotatedQuery(), Sort.unsorted(), null,
method.getResultProcessor().getReturnedType());

View File

@@ -29,6 +29,7 @@ import org.assertj.core.api.Assertions;
import org.assertj.core.util.Arrays;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -40,9 +41,8 @@ import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -116,8 +116,7 @@ class AbstractStringBasedJpaQueryUnitTests {
Query query = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class);
return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery(),
new SpelExpressionParser());
return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery());
}
@@ -127,7 +126,7 @@ class AbstractStringBasedJpaQueryUnitTests {
private final MultiValueMap<String, Arguments> capturedArguments = new LinkedMultiValueMap<>(3);
InvocationCapturingStringQueryStub(Method targetMethod, JpaQueryMethod queryMethod, String queryString,
@Nullable String countQueryString, SpelExpressionParser parser) {
@Nullable String countQueryString) {
super(queryMethod, new Supplier<EntityManager>() {
@Override
@@ -142,7 +141,7 @@ class AbstractStringBasedJpaQueryUnitTests {
return em;
}
}.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class),
Mockito.mock(QueryMethodEvaluationContextProvider.class), parser);
ValueExpressionDelegate.create());
this.targetMethod = targetMethod;
}

View File

@@ -27,9 +27,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.expression.ValueExpressionParser;
import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* Unit tests for {@link ExpressionBasedStringQuery}.
@@ -46,7 +47,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
@MockitoSettings(strictness = Strictness.LENIENT)
class ExpressionBasedStringQueryUnitTests {
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
private static final ValueExpressionParser PARSER = ValueExpressionParser.create();
@Mock JpaEntityMetadata<?> metadata;
@BeforeEach
@@ -58,14 +59,14 @@ class ExpressionBasedStringQueryUnitTests {
void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() {
String source = "select u from #{#entityName} u where u.firstname like :firstname";
StringQuery query = new ExpressionBasedStringQuery(source, metadata, SPEL_PARSER, false);
StringQuery query = new ExpressionBasedStringQuery(source, metadata, PARSER, false);
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname");
}
@Test // DATAJPA-424
void renderAliasInExpressionQueryCorrectly() {
StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata, SPEL_PARSER, true);
StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata, PARSER, true);
assertThat(query.getAlias()).isEqualTo("u");
assertThat(query.getQueryString()).isEqualTo("select u from User u");
}
@@ -78,7 +79,7 @@ class ExpressionBasedStringQueryUnitTests {
+ "AND (LOWER(n.server) LIKE LOWER(:#{#networkRequest.server})) OR :#{#networkRequest.server} IS NULL "
+ "AND (n.createdAt >= :#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=:#{#networkRequest.createdTime.endDateTime}) "
+ "AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})",
metadata, SPEL_PARSER, false);
metadata, PARSER, false);
assertThat(query.getParameterBindings()).hasSize(8);
}
@@ -91,7 +92,7 @@ class ExpressionBasedStringQueryUnitTests {
+ "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)"
+ "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})"
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
metadata, SPEL_PARSER, false);
metadata, PARSER, false);
assertThat(query.getParameterBindings()).hasSize(8);
}
@@ -104,7 +105,7 @@ class ExpressionBasedStringQueryUnitTests {
+ "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)"
+ "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})"
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
metadata, SPEL_PARSER, true);
metadata, PARSER, true);
assertThat(query.isNativeQuery()).isFalse();
}
@@ -112,7 +113,7 @@ class ExpressionBasedStringQueryUnitTests {
@Test
void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() {
StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, SPEL_PARSER, true);
StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, PARSER, true);
assertThat(query.isNativeQuery()).isFalse();
}
@@ -120,7 +121,7 @@ class ExpressionBasedStringQueryUnitTests {
@Test
void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() {
StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, SPEL_PARSER, true);
StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, PARSER, true);
assertThat(query.isNativeQuery()).isTrue();
}
@@ -129,7 +130,7 @@ class ExpressionBasedStringQueryUnitTests {
void namedExpressionsShouldCreateLikeBindings() {
StringQuery query = new ExpressionBasedStringQuery(
"select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, SPEL_PARSER,
"select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, PARSER,
false);
assertThat(query.hasParameterBindings()).isTrue();
@@ -154,7 +155,7 @@ class ExpressionBasedStringQueryUnitTests {
void indexedExpressionsShouldCreateLikeBindings() {
StringQuery query = new ExpressionBasedStringQuery(
"select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, SPEL_PARSER,
"select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, PARSER,
false);
assertThat(query.hasParameterBindings()).isTrue();
@@ -179,7 +180,7 @@ class ExpressionBasedStringQueryUnitTests {
public void doesTemplatingWhenEntityNameSpelIsPresent() {
StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from #{#entityName} u",
metadata, SPEL_PARSER, false);
metadata, PARSER, false);
assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u");
}
@@ -188,7 +189,7 @@ class ExpressionBasedStringQueryUnitTests {
public void doesNoTemplatingWhenEntityNameSpelIsNotPresent() {
StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from User u", metadata,
SPEL_PARSER, false);
PARSER, false);
assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u");
}
@@ -197,7 +198,7 @@ class ExpressionBasedStringQueryUnitTests {
public void doesTemplatingWhenEntityNameSpelIsPresentForBindParameter() {
StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u where name = :#{#something}",
metadata, SPEL_PARSER, false);
metadata, PARSER, false);
assertThat(query.getQueryString()).isEqualTo("select u from User u where name = :__$synthetic$__1");
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding;
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
@@ -29,44 +30,45 @@ import org.springframework.data.repository.query.parser.Part.Type;
* @author Oliver Gierke
* @author Thomas Darimont
* @author Jens Schauder
* @author Mark Paluch
*/
class LikeBindingUnitTests {
private static void assertAugmentedValue(Type type, Object value) {
LikeParameterBinding binding = new LikeParameterBinding(BindingIdentifier.of("foo"),
ParameterOrigin.ofExpression("foo"), type);
ParameterOrigin.ofParameter(1), type);
assertThat(binding.prepare("value")).isEqualTo(value);
}
@Test
void rejectsNullName() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new LikeParameterBinding(null, ParameterOrigin.ofExpression(""), Type.CONTAINING));
.isThrownBy(() -> new LikeParameterBinding(null, ParameterOrigin.ofParameter(0), Type.CONTAINING));
}
@Test
void rejectsEmptyName() {
assertThatIllegalArgumentException().isThrownBy(
() -> new LikeParameterBinding(BindingIdentifier.of(""), ParameterOrigin.ofExpression(""), Type.CONTAINING));
() -> new LikeParameterBinding(BindingIdentifier.of(""), ParameterOrigin.ofParameter(0), Type.CONTAINING));
}
@Test
void rejectsNullType() {
assertThatIllegalArgumentException().isThrownBy(
() -> new LikeParameterBinding(BindingIdentifier.of("foo"), ParameterOrigin.ofExpression("foo"), null));
() -> new LikeParameterBinding(BindingIdentifier.of("foo"), ParameterOrigin.ofParameter(0), null));
}
@Test
void rejectsInvalidType() {
assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding(BindingIdentifier.of("foo"),
ParameterOrigin.ofExpression("foo"), Type.SIMPLE_PROPERTY));
ParameterOrigin.ofParameter(0), Type.SIMPLE_PROPERTY));
}
@Test
void rejectsInvalidPosition() {
assertThatIllegalArgumentException().isThrownBy(
() -> new LikeParameterBinding(BindingIdentifier.of(0), ParameterOrigin.ofExpression(""), Type.CONTAINING));
() -> new LikeParameterBinding(BindingIdentifier.of(0), ParameterOrigin.ofParameter(0), Type.CONTAINING));
}
@Test

View File

@@ -39,8 +39,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.ReflectionUtils;
/**
@@ -83,8 +82,7 @@ class NativeJpaQueryUnitTests {
Query annotation = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class);
NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, annotation.value(), annotation.countQuery(),
QueryRewriter.IdentityQueryRewriter.INSTANCE, QueryMethodEvaluationContextProvider.DEFAULT,
new SpelExpressionParser());
QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create());
return query;
}

View File

@@ -52,10 +52,9 @@ import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
/**
@@ -77,8 +76,6 @@ import org.springframework.lang.Nullable;
class SimpleJpaQueryUnitTests {
private static final String USER_QUERY = "select u from User u";
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private static final QueryMethodEvaluationContextProvider EVALUATION_CONTEXT_PROVIDER = QueryMethodEvaluationContextProvider.DEFAULT;
private JpaQueryMethod method;
@@ -123,7 +120,7 @@ class SimpleJpaQueryUnitTests {
when(em.createQuery("foo", Long.class)).thenReturn(typedQuery);
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null,
QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER, PARSER);
QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create());
assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {})))
.isEqualTo(typedQuery);
@@ -138,7 +135,7 @@ class SimpleJpaQueryUnitTests {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null,
QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER, PARSER);
QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create());
jpaQuery.createCountQuery(
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
@@ -154,7 +151,7 @@ class SimpleJpaQueryUnitTests {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em,
queryMethod.getAnnotatedQuery(), null, QueryRewriter.IdentityQueryRewriter.INSTANCE,
EVALUATION_CONTEXT_PROVIDER);
ValueExpressionDelegate.create());
assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class);
@@ -174,7 +171,7 @@ class SimpleJpaQueryUnitTests {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em,
queryMethod.getAnnotatedQuery(), null, QueryRewriter.IdentityQueryRewriter.INSTANCE,
EVALUATION_CONTEXT_PROVIDER);
ValueExpressionDelegate.create());
assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class);
@@ -287,7 +284,7 @@ class SimpleJpaQueryUnitTests {
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u",
"select count(u.id) from #{#entityName} u", QueryRewriter.IdentityQueryRewriter.INSTANCE,
EVALUATION_CONTEXT_PROVIDER, PARSER);
ValueExpressionDelegate.create());
jpaQuery.createCountQuery(
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
@@ -302,7 +299,7 @@ class SimpleJpaQueryUnitTests {
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) {
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString,
QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER);
QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create());
}
private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional<String> countQueryString) {

View File

@@ -415,13 +415,15 @@ class StringQueryUnitTests {
assertNamedBinding(InParameterBinding.class, "ab1babc생일233", bindings.get(0));
}
@Test // DATAJPA-712
@Test // DATAJPA-712, GH-3619
void shouldReplaceAllNamedExpressionParametersWithInClause() {
StringQuery query = new StringQuery("select a from A a where a.b in :#{#bs} and a.c in :#{#cs}", true);
StringQuery query = new StringQuery(
"select a from A a where a.b in :#{#bs} and a.c in :#{#cs} and a.d in :${foo.bar}", true);
String queryString = query.getQueryString();
assertThat(queryString).isEqualTo("select a from A a where a.b in :__$synthetic$__1 and a.c in :__$synthetic$__2");
assertThat(queryString).isEqualTo(
"select a from A a where a.b in :__$synthetic$__1 and a.c in :__$synthetic$__2 and a.d in :__$synthetic$__3");
}
@Test // DATAJPA-712
@@ -435,15 +437,21 @@ class StringQueryUnitTests {
.isEqualTo("select a from A a where a.b LIKE :__$synthetic$__1 and a.c LIKE :__$synthetic$__2");
}
@Test // DATAJPA-712
@Test // DATAJPA-712, GH-3619
void shouldReplaceAllPositionExpressionParametersWithInClause() {
StringQuery query = new StringQuery("select a from A a where a.b in ?#{#bs} and a.c in ?#{#cs}", true);
StringQuery query = new StringQuery("select a from A a where a.b in ?#{#bs} and a.c in ?#{#cs} and a.d in ?${foo}",
true);
String queryString = query.getQueryString();
assertThat(queryString).isEqualTo("select a from A a where a.b in ?1 and a.c in ?2");
assertThat(((Expression) query.getParameterBindings().get(0).getOrigin()).expression()).isEqualTo("#bs");
assertThat(((Expression) query.getParameterBindings().get(1).getOrigin()).expression()).isEqualTo("#cs");
assertThat(queryString).isEqualTo("select a from A a where a.b in ?1 and a.c in ?2 and a.d in ?3");
assertThat(((Expression) query.getParameterBindings().get(0).getOrigin()).expression().getExpressionString())
.isEqualTo("#bs");
assertThat(((Expression) query.getParameterBindings().get(1).getOrigin()).expression().getExpressionString())
.isEqualTo("#cs");
assertThat(((Expression) query.getParameterBindings().get(2).getOrigin()).expression().getExpressionString())
.isEqualTo("${foo}");
}
@Test // DATAJPA-864
@@ -480,7 +488,7 @@ class StringQueryUnitTests {
for (ParameterBinding binding : bindings) {
assertThat(binding.getName()).isNotNull();
assertThat(query.getQueryString()).contains(binding.getName());
assertThat(((Expression) binding.getOrigin()).expression()).isEqualTo("#exp");
assertThat(((Expression) binding.getOrigin()).expression().getExpressionString()).isEqualTo("#exp");
}
}

View File

@@ -617,6 +617,10 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
@Query("select u from User u where u.lastname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
List<User> findContainingEscaped(String namePart);
// GH-3619
@Query("select u from User u where u.lastname like ?${query.lastname:empty}")
List<User> findWithPropertyPlaceholder();
// DATAJPA-1303
List<User> findByAttributesIgnoreCaseIn(Collection<String> attributes);

View File

@@ -9,6 +9,7 @@
** xref:jpa/entity-persistence.adoc[]
** xref:repositories/query-methods-details.adoc[]
** xref:jpa/query-methods.adoc[]
** xref:jpa/value-expressions.adoc[]
** xref:repositories/projections.adoc[]
** xref:jpa/stored-procedures.adoc[]
** xref:jpa/specifications.adoc[]

View File

@@ -12,7 +12,7 @@ Derived queries with the predicates `IsStartingWith`, `StartingWith`, `StartsWit
`IsNotContaining`, `NotContaining`, `NotContains`, `IsContaining`, `Containing`, `Contains` the respective arguments for these queries will get sanitized.
This means if the arguments actually contain characters recognized by `LIKE` as wildcards these will get escaped so they match only as literals.
The escape character used can be configured by setting the `escapeCharacter` of the `@EnableJpaRepositories` annotation.
Compare with xref:jpa/query-methods.adoc#jpa.query.spel-expressions[Using SpEL Expressions].
Compare with xref:jpa/query-methods.adoc#jpa.query.spel-expressions[Using Value Expressions].
[[jpa.query-methods.declared-queries]]
=== Declared Queries
@@ -401,13 +401,23 @@ NOTE: The method parameters are switched according to their order in the defined
NOTE: As of version 4, Spring fully supports Java 8s parameter name discovery based on the `-parameters` compiler flag. By using this flag in your build as an alternative to debug information, you can omit the `@Param` annotation for named parameters.
[[jpa.query.spel-expressions]]
== Using SpEL Expressions
== Using Expressions
As of Spring Data JPA release 1.4, we support the usage of restricted SpEL template expressions in manually defined queries that are defined with `@Query`. Upon the query being run, these expressions are evaluated against a predefined set of variables. Spring Data JPA supports a variable called `entityName`. Its usage is `select x from #{#entityName} x`. It inserts the `entityName` of the domain type associated with the given repository. The `entityName` is resolved as follows: If the domain type has set the name property on the `@Entity` annotation, it is used. Otherwise, the simple class-name of the domain type is used.
We support the usage of restricted expressions in manually defined queries that are defined with `@Query`.
Upon the query being run, these expressions are evaluated against a predefined set of variables.
NOTE: If you are not familiar with Value Expressions, please refer to xref:jpa/value-expressions.adoc[] to learn about SpEL Expressions and Property Placeholders.
Spring Data JPA supports a variable called `entityName`.
Its usage is `select x from #{#entityName} x`.
It inserts the `entityName` of the domain type associated with the given repository.
The `entityName` is resolved as follows:
* If the domain type has set the name property on the `@Entity` annotation, it is used.
* Otherwise, the simple class-name of the domain type is used.
The following example demonstrates one use case for the `+#{#entityName}+` expression in a query string where you want to define a repository interface with a query method and a manually defined query:
.Using SpEL expressions in repository query methods - entityName
.Using SpEL expressions in repository query methods: entityName
====
[source, java]
----
@@ -431,13 +441,16 @@ public interface UserRepository extends JpaRepository<User,Long> {
To avoid stating the actual entity name in the query string of a `@Query` annotation, you can use the `+#{#entityName}+` variable.
NOTE: The `entityName` can be customized by using the `@Entity` annotation. Customizations in `orm.xml` are not supported for the SpEL expressions.
NOTE: The `entityName` can be customized by using the `@Entity` annotation.
Customizations in `orm.xml` are not supported for the SpEL expressions.
Of course, you could have just used `User` in the query declaration directly, but that would require you to change the query as well. The reference to `#entityName` picks up potential future remappings of the `User` class to a different entity name (for example, by using `@Entity(name = "MyUser")`.
Of course, you could have just used `User` in the query declaration directly, but that would require you to change the query as well.
The reference to `#entityName` picks up potential future remappings of the `User` class to a different entity name (for example, by using `@Entity(name = "MyUser")`.
Another use case for the `#{#entityName}` expression in a query string is if you want to define a generic repository interface with specialized repository interfaces for a concrete domain type. To not repeat the definition of custom query methods on the concrete interfaces, you can use the entity name expression in the query string of the `@Query` annotation in the generic repository interface, as shown in the following example:
Another use case for the `#{#entityName}` expression in a query string is if you want to define a generic repository interface with specialized repository interfaces for a concrete domain type.
To not repeat the definition of custom query methods on the concrete interfaces, you can use the entity name expression in the query string of the `@Query` annotation in the generic repository interface, as shown in the following example:
.Using SpEL expressions in repository query methods - entityName with inheritance
.Using SpEL expressions in Repository Query Methods: entityName with Inheritance
====
[source, java]
----
@@ -463,13 +476,15 @@ public interface ConcreteRepository
----
====
In the preceding example, the `MappedTypeRepository` interface is the common parent interface for a few domain types extending `AbstractMappedType`. It also defines the generic `findAllByAttribute(…)` method, which can be used on instances of the specialized repository interfaces. If you now invoke `findByAllAttribute(…)` on `ConcreteRepository`, the query becomes `select t from ConcreteType t where t.attribute = ?1`.
In the preceding example, the `MappedTypeRepository` interface is the common parent interface for a few domain types extending `AbstractMappedType`.
It also defines the generic `findAllByAttribute(…)` method, which can be used on instances of the specialized repository interfaces.
If you now invoke `findByAllAttribute(…)` on `ConcreteRepository`, the query becomes `select t from ConcreteType t where t.attribute = ?1`.
SpEL expressions to manipulate arguments may also be used to manipulate method arguments.
In these SpEL expressions the entity name is not available, but the arguments are.
You can also use Expressions to control arguments may also be used to control method arguments.
In these expressions the entity name is not available, but the arguments are.
They can be accessed by name or index as demonstrated in the following example.
.Using SpEL expressions in repository query methods - accessing arguments.
.Using Value Expressions in Repository Query Methods: Accessing Arguments
====
[source, java]
----
@@ -482,7 +497,7 @@ For `like`-conditions one often wants to append `%` to the beginning or the end
This can be done by appending or prefixing a bind parameter marker or a SpEL expression with `%`.
Again the following example demonstrates this.
.Using SpEL expressions in repository query methods - wildcard shortcut.
.Using Value Expressions in Repository Query Methods: Wildcard shortcut
====
[source, java]
----
@@ -496,8 +511,7 @@ For this purpose the `escape(String)` method is made available in the SpEL conte
It prefixes all instances of `_` and `%` in the first argument with the single character from the second argument.
In combination with the `escape` clause of the `like` expression available in JPQL and standard SQL this allows easy cleaning of bind parameters.
.Using SpEL expressions in repository query methods - sanitizing input values.
.Using Value Expressions in Repository Query Methods: Sanitizing Input Values
====
[source, java]
----
@@ -511,6 +525,19 @@ The escape character used can be configured by setting the `escapeCharacter` of
Note that the method `escape(String)` available in the SpEL context will only escape the SQL and JPQL standard wildcards `_` and `%`.
If the underlying database or the JPA implementation supports additional wildcards these will not get escaped.
.Using Value Expressions in Repository Query Methods: Configuration Properties
====
[source,java]
----
@Query("select u from User u where u.applicationName = ?${spring.application.name:unknown}")
List<User> findContainingEscaped(String namePart);
----
====
You can refer in your query methods also to configuration property names including fallbacks if you wish to resolve a property from `Environment` during runtime.
The property is being evaluated upon query execution.
Typically, property placeholders resolve to String-like values.
[[jpa.query.other-methods]]
== Other Methods

View File

@@ -0,0 +1 @@
include::{commons}@data-commons::page$value-expressions.adoc[]