diff --git a/pom.xml b/pom.xml index 2311b378e6..93a502784e 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-3619-SNAPSHOT pom Spring Data JPA Parent @@ -41,7 +41,7 @@ 5.0 8.0.33 42.6.0 - 3.4.0-SNAPSHOT + 3.4.0-GH-3049-SNAPSHOT 0.10.3 org.hibernate diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 8b836ae2f3..60fc0a4b6a 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 3.4.0-SNAPSHOT + 3.4.0-GH-3619-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-3619-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index e9e1754bc3..c04612c867 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-3619-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-performance/pom.xml b/spring-data-jpa-performance/pom.xml index fa836f34c6..d6f3df8a50 100644 --- a/spring-data-jpa-performance/pom.xml +++ b/spring-data-jpa-performance/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-jpa-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-3619-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 3006702796..df991fc501 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-jpa - 3.4.0-SNAPSHOT + 3.4.0-GH-3619-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-jpa-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-3619-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index c60da04424..0d257fe5a2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -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; @@ -50,12 +50,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final DeclaredQuery query; private final Lazy 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 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 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri return this.createBinder(this.countQuery.get()); }); - this.parser = parser; this.queryRewriter = queryRewriter; JpaParameters parameters = method.getParameters(); @@ -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 diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java index 411c2662d5..8066504ca3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java @@ -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 @@ 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); } @@ -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"); @@ -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; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java index 8f1d3247df..a998e1a086 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java @@ -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; /** @@ -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); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index 8cf8d12125..136a16b271 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -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; @@ -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; @@ -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}. @@ -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 @@ -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); @@ -267,28 +271,47 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer * @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"); - - 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)); - } + Assert.notNull(delegate, "ValueExpressionDelegate must not be null"); + + 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)); + }; } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index 7b8a018a8c..61c4c6ef19 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -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; @@ -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 annotation = annotations.get(NativeQuery.class); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index 898cd73936..e5122e93d3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java @@ -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; /** @@ -86,7 +86,7 @@ static ParameterBinder createCriteriaBinder(JpaParameters parameters, List bindings = query.getParameterBindings(); QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser, - evaluationContextProvider, parameters); + evaluationContextProvider); QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index 4778943f68..493f474f6f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -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; @@ -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); } @@ -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() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index 9e27184582..294ec89c88 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -21,6 +21,10 @@ 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.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 @@ static QueryParameterSetterFactory forCriteriaQuery(JpaParameters parameters, Li * @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 @@ static JpaParameter findParameterForBinding(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 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla 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 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla * @return the result of the evaluation. */ @Nullable - private Object evaluateExpression(Expression expression, JpaParametersParameterAccessor accessor) { - - EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameters, accessor.getValues()); + private Object evaluateExpression(ValueExpression expression, JpaParametersParameterAccessor accessor) { - return expression.getValue(context, Object.class); + ValueEvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(accessor.getValues()); + return expression.evaluate(evaluationContext); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index 5174168501..16fa3c30e0 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -19,9 +19,8 @@ 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 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String * @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); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index 3b365926db..471ede0403 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -26,13 +26,14 @@ 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 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que 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 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que while (matcher.find()) { - if (spelExtractor.isQuoted(matcher.start())) { + if (parsedQuery.isQuoted(matcher.start())) { continue; } @@ -282,7 +284,8 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que 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 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que 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 @@ private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean p String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":"; BiFunction 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 diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index bffdafd46b..7ecd163244 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -56,10 +56,11 @@ 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 @@ protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFa @Override protected Optional 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 JpaEntityInformation getEntityInformation(Class domainClass) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index 6a8c06789d..259420a09a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java @@ -309,6 +309,20 @@ void escapingInLikeSpelsInThePresenceOfEscapedWildcards() { 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() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java index 6e6c605114..9cb74dcaa0 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java @@ -26,6 +26,7 @@ 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.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 @@ void createsNormalQueryForJpaManagedReturnTypes() throws Exception { 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()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java index 5a33a4bf59..b2e6ba4fce 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java @@ -29,6 +29,7 @@ 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.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 @@ static InvocationCapturingStringQueryStub forMethod(Class repository, String 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 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu private final MultiValueMap capturedArguments = new LinkedMultiValueMap<>(3); InvocationCapturingStringQueryStub(Method targetMethod, JpaQueryMethod queryMethod, String queryString, - @Nullable String countQueryString, SpelExpressionParser parser) { + @Nullable String countQueryString) { super(queryMethod, new Supplier() { @Override @@ -142,7 +141,7 @@ public EntityManager get() { return em; } }.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class), - Mockito.mock(QueryMethodEvaluationContextProvider.class), parser); + ValueExpressionDelegate.create()); this.targetMethod = targetMethod; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java index e92c16eb88..d651db1393 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java @@ -27,9 +27,10 @@ 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 @@ @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 @@ void setUp() { 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 @@ void shouldDetectBindParameterCountCorrectly() { + "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 @@ void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() { + "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 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { + "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 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { @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 @@ void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { @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 @@ void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { 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 @@ void namedExpressionsShouldCreateLikeBindings() { 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 @@ void indexedExpressionsShouldCreateLikeBindings() { 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 @@ public void doesTemplatingWhenEntityNameSpelIsPresent() { 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 @@ public void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { 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"); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java index 54b2e8bad3..c67b1ce836 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java @@ -18,6 +18,7 @@ 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 @@ * @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 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java index 67c61b9305..7a9cf35d1f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java @@ -39,8 +39,7 @@ 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 @@ private NativeJpaQuery getQuery(Class repository, String method, Class... 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; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 014dee96f7..e5a6b04334 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -52,10 +52,9 @@ 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 @@ 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 @@ void prefersDeclaredCountQueryOverCreatingOne() throws Exception { 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 @@ void doesNotApplyPaginationToCountQuery() throws Exception { 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 @@ void discoversNativeQuery() throws Exception { 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 @@ void discoversNativeQueryFromNativeQueryInterface() throws Exception { 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 @@ void resolvesExpressionInCountQuery() throws Exception { 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 @@ private AbstractJpaQuery createJpaQuery(Method method) { 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 countQueryString) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java index f54272b0a7..b7cb49db74 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java @@ -415,13 +415,15 @@ void detectsInBindingWithSpecialCharactersAndWordCharactersMixedInParentheses() 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 @@ void shouldReplaceExpressionWithLikeParameters() { .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 @@ void bindingsMatchQueryForIdenticalSpelExpressions() { 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"); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 86d97ea40c..01526cd0bf 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -617,6 +617,10 @@ List findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity @Query("select u from User u where u.lastname like %?#{escape([0])}% escape ?#{escapeCharacter()}") List findContainingEscaped(String namePart); + // GH-3619 + @Query("select u from User u where u.lastname like ?${query.lastname:empty}") + List findWithPropertyPlaceholder(); + // DATAJPA-1303 List findByAttributesIgnoreCaseIn(Collection attributes); diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 501232f800..1e44d61f58 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -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[] diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index 33cdd8bd08..80737f73ad 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -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 @@ -390,13 +390,23 @@ NOTE: The method parameters are switched according to their order in the defined NOTE: As of version 4, Spring fully supports Java 8’s 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] ---- @@ -420,13 +430,16 @@ public interface UserRepository extends JpaRepository { 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] ---- @@ -452,13 +465,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] ---- @@ -471,7 +486,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] ---- @@ -485,8 +500,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] ---- @@ -500,6 +514,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 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 diff --git a/src/main/antora/modules/ROOT/pages/jpa/value-expressions.adoc b/src/main/antora/modules/ROOT/pages/jpa/value-expressions.adoc new file mode 100644 index 0000000000..6356a46265 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/jpa/value-expressions.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$value-expressions.adoc[]