Skip to content

Commit

Permalink
Add support for Value Expressions.
Browse files Browse the repository at this point in the history
Closes #3619
  • Loading branch information
mp911de committed Sep 27, 2024
1 parent f8503e9 commit b13276a
Show file tree
Hide file tree
Showing 23 changed files with 237 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@

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;
Expand All @@ -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
Expand All @@ -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());
}

Expand All @@ -99,7 +98,6 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
return this.createBinder(this.countQuery.get());
});

this.parser = parser;
this.queryRewriter = queryRewriter;

JpaParameters parameters = method.getParameters();
Expand Down Expand Up @@ -140,8 +138,8 @@ protected ParameterBinder createBinder() {
}

protected ParameterBinder createBinder(DeclaredQuery query) {
return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, parser,
evaluationContextProvider);
return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query,
valueExpressionDelegate, valueExpressionContextProvider);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
}
Expand All @@ -74,7 +75,7 @@ public ExpressionBasedStringQuery(String query, JpaEntityMetadata<?> metadata, S
* @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);
}

Expand All @@ -84,7 +85,7 @@ static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata<?>
* @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");
Expand All @@ -99,9 +100,9 @@ private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEnti

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@

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;

/**
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

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;
Expand All @@ -30,7 +32,9 @@
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;
Expand Down Expand Up @@ -135,7 +139,7 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer
*/
private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy {

private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ValueExpressionDelegate valueExpressionDelegate;

/**
* Creates a new {@link DeclaredQueryLookupStrategy}.
Expand All @@ -145,11 +149,11 @@ private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStra
* @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
Expand All @@ -168,13 +172,13 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer
}

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);
Expand Down Expand Up @@ -271,20 +275,39 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer
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,
return new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate,
queryRewriterProvider);
case CREATE_IF_NOT_FOUND:
return new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory,
new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape),
new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider, queryRewriterProvider),
new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider),
queryRewriterProvider);
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 @@ -25,10 +25,9 @@
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;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
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;

/**
Expand Down Expand Up @@ -86,7 +86,7 @@ static ParameterBinder createCriteriaBinder(JpaParameters parameters, List<Param
* {@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");
Expand All @@ -95,7 +95,7 @@ static ParameterBinder createQueryAwareBinder(JpaParameters parameters, Declared

List<ParameterBinding> bindings = query.getParameterBindings();
QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser,
evaluationContextProvider, parameters);
evaluationContextProvider);

QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
Expand Down Expand Up @@ -493,12 +494,12 @@ public String toString() {
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);
}

Expand Down Expand Up @@ -562,7 +563,7 @@ static MethodInvocationArgument ofParameter(BindingIdentifier identifier) {
* @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() {
Expand Down
Loading

0 comments on commit b13276a

Please sign in to comment.