From e2446caf38541ff25e6e5a31ea01c0a11d2db19b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 17 Mar 2025 15:19:59 +0100 Subject: [PATCH] Polishing. Introduce refined names: EntityQuery, TemplatedQuery, ParametrizedQuery, QueryProvider. Return QueryProvider where possible. Introduce rewrite as concept on DeclaredQuery to retain its nature and track the origin of the query rewriting. Move methods solely used in tests to TestDefaultEntityQuery. Remove unused methods, fix naming, group DeclaredQuery implementations in DeclaredQueries. Add documentation. See #3622 Original pull request: #3527 --- .../repository/query/HqlParserBenchmarks.java | 7 +- .../JSqlParserQueryEnhancerBenchmarks.java | 7 +- .../repository/JpaSpecificationExecutor.java | 11 - .../data/jpa/repository/NativeQuery.java | 1 + .../data/jpa/repository/Query.java | 1 + .../config/EnableJpaRepositories.java | 1 + .../query/AbstractStringBasedJpaQuery.java | 95 ++-- .../jpa/repository/query/BindableQuery.java | 67 --- .../jpa/repository/query/DeclaredQueries.java | 148 +++++ .../jpa/repository/query/DeclaredQuery.java | 41 +- .../repository/query/DefaultEntityQuery.java | 159 ++++++ .../query/DefaultQueryEnhancer.java | 32 +- .../query/EmptyIntrospectedQuery.java | 53 +- .../jpa/repository/query/EntityQuery.java | 91 ++- .../query/JSqlParserQueryEnhancer.java | 25 +- .../query/JpaQueryConfiguration.java | 1 + .../repository/query/JpaQueryEnhancer.java | 61 +- .../query/JpaQueryLookupStrategy.java | 31 +- .../jpa/repository/query/JpaQueryMethod.java | 54 +- .../data/jpa/repository/query/NamedQuery.java | 13 +- .../jpa/repository/query/NativeJpaQuery.java | 35 +- .../query/ParameterBinderFactory.java | 10 +- ...ectedQuery.java => ParametrizedQuery.java} | 42 +- ...tringQuery.java => PreprocessedQuery.java} | 522 ++++++++---------- .../jpa/repository/query/QueryEnhancer.java | 68 +-- .../query/QueryEnhancerFactories.java | 23 +- .../query/QueryEnhancerFactory.java | 8 +- .../query/QueryEnhancerSelector.java | 4 +- .../query/QueryParameterSetterFactory.java | 14 +- .../{NativeQuery.java => QueryProvider.java} | 29 +- .../data/jpa/repository/query/QueryUtils.java | 12 +- .../jpa/repository/query/SimpleJpaQuery.java | 14 +- .../jpa/repository/query/StructuredQuery.java | 24 - ...edStringQuery.java => TemplatedQuery.java} | 57 +- ...ctStringBasedJpaQueryIntegrationTests.java | 5 +- .../AbstractStringBasedJpaQueryUnitTests.java | 9 +- ....java => DefaultEntityQueryUnitTests.java} | 157 +++--- .../query/DefaultQueryEnhancerUnitTests.java | 11 +- .../EqlParserQueryEnhancerUnitTests.java | 2 +- .../query/EqlQueryTransformerTests.java | 9 +- .../HqlParserQueryEnhancerUnitTests.java | 2 +- .../query/HqlQueryTransformerTests.java | 9 +- .../JSqlParserQueryEnhancerUnitTests.java | 128 ++--- .../JpqlParserQueryEnhancerUnitTests.java | 2 +- .../query/JpqlQueryTransformerTests.java | 10 +- .../query/NativeJpaQueryUnitTests.java | 11 +- .../ParameterBindingParserUnitTests.java | 3 +- .../query/QueryEnhancerFactoryUnitTests.java | 9 +- .../query/QueryEnhancerTckTests.java | 8 +- .../query/QueryEnhancerUnitTests.java | 285 +++++----- .../QueryParameterSetterFactoryUnitTests.java | 11 +- .../query/SimpleJpaQueryUnitTests.java | 29 +- ...ests.java => TemplatedQueryUnitTests.java} | 68 +-- .../repository/query/TestEntityQuery.java} | 30 +- .../modules/ROOT/pages/jpa/query-methods.adoc | 129 ++++- 55 files changed, 1490 insertions(+), 1198 deletions(-) delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{IntrospectedQuery.java => ParametrizedQuery.java} (64%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{StringQuery.java => PreprocessedQuery.java} (61%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{NativeQuery.java => QueryProvider.java} (63%) delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{ExpressionBasedStringQuery.java => TemplatedQuery.java} (63%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/{StringQueryUnitTests.java => DefaultEntityQueryUnitTests.java} (85%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/{ExpressionBasedStringQueryUnitTests.java => TemplatedQueryUnitTests.java} (71%) rename spring-data-jpa/src/{main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java => test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java} (53%) diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java index ecbb4eb23..d1465ed1b 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java @@ -27,6 +27,8 @@ import org.openjdk.jmh.annotations.Timeout; import org.openjdk.jmh.annotations.Warmup; import org.springframework.data.domain.Sort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; /** * @author Mark Paluch @@ -44,6 +46,7 @@ public class HqlParserBenchmarks { DeclaredQuery query; Sort sort = Sort.by("foo"); QueryEnhancer enhancer; + QueryEnhancer.QueryRewriteInformation rewriteInformation; @Setup(Level.Iteration) public void doSetup() { @@ -57,12 +60,14 @@ public class HqlParserBenchmarks { query = DeclaredQuery.jpqlQuery(s); enhancer = QueryEnhancerFactory.forQuery(query).create(query); + rewriteInformation = new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); } } @Benchmark public Object measure(BenchmarkParameters parameters) { - return parameters.enhancer.applySorting(parameters.sort); + return parameters.enhancer.rewrite(parameters.rewriteInformation); } } diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java index a5c9cdce2..f4121c28e 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java @@ -29,6 +29,8 @@ import org.openjdk.jmh.annotations.Timeout; import org.openjdk.jmh.annotations.Warmup; import org.springframework.data.domain.Sort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; /** * @author Mark Paluch @@ -46,6 +48,7 @@ public class JSqlParserQueryEnhancerBenchmarks { JSqlParserQueryEnhancer enhancer; Sort sort = Sort.by("foo"); private byte[] serialized; + private QueryEnhancer.QueryRewriteInformation rewriteInformation; @Setup(Level.Iteration) public void doSetup() throws IOException { @@ -57,12 +60,14 @@ public class JSqlParserQueryEnhancerBenchmarks { union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"""; enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.nativeQuery(s)); + rewriteInformation = new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); } } @Benchmark public Object applySortWithParsing(BenchmarkParameters p) { - return p.enhancer.applySorting(p.sort); + return p.enhancer.rewrite(p.rewriteInformation); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index ffd6f5552..536ff5bca 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -15,27 +15,17 @@ */ package org.springframework.data.jpa.repository; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Root; - -import java.util.Arrays; -import java.util.Collection; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.Function; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; - -import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.DeleteSpecification; import org.springframework.data.jpa.domain.PredicateSpecification; @@ -115,7 +105,6 @@ public interface JpaSpecificationExecutor { * Returns a {@link Page} of entities matching the given {@link Specification}. *

* Supports counting the total number of entities matching the {@link Specification}. - *

* * @param spec can be {@literal null}, if no {@link Specification} is given all entities matching {@code } will be * selected. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java index d10c90b68..d12036c74 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java @@ -94,4 +94,5 @@ public @interface NativeQuery { * Name of the {@link jakarta.persistence.SqlResultSetMapping @SqlResultSetMapping(name)} to apply for this query. */ String sqlResultSetMapping() default ""; + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java index 12ff41bb7..4405d29bb 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java @@ -90,4 +90,5 @@ public @interface Query { * @since 3.0 */ Class queryRewriter() default QueryRewriter.IdentityQueryRewriter.class; + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java index 68a173f05..22f32ed2d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java @@ -178,4 +178,5 @@ public @interface EnableJpaRepositories { * @since 4.0 */ Class queryEnhancerSelector() default QueryEnhancerSelector.DefaultQueryEnhancerSelector.class; + } 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 e4216bdd7..148567a9e 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 @@ -23,9 +23,9 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.data.domain.Pageable; - import org.jspecify.annotations.Nullable; + +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; @@ -54,9 +54,9 @@ import org.springframework.util.StringUtils; */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { - private final StringQuery query; + private final EntityQuery query; private final Map, Boolean> knownProjections = new ConcurrentHashMap<>(); - private final Lazy countQuery; + private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; @@ -70,25 +70,42 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * @param method must not be {@literal null}. * @param em must not be {@literal null}. * @param queryString must not be {@literal null}. - * @param countQueryString must not be {@literal null}. + * @param countQuery can be {@literal null} if not defined. * @param queryConfiguration must not be {@literal null}. */ - public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, + AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) { + this(method, em, method.getDeclaredQuery(queryString), + countQueryString != null ? method.getDeclaredQuery(countQueryString) : null, queryConfiguration); + } + + /** + * Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and + * query {@link String}. + * + * @param method must not be {@literal null}. + * @param em must not be {@literal null}. + * @param query must not be {@literal null}. + * @param countQuery can be {@literal null}. + * @param queryConfiguration must not be {@literal null}. + */ + public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query, + @Nullable DeclaredQuery countQuery, JpaQueryConfiguration queryConfiguration) { super(method, em); - Assert.hasText(queryString, "Query string must not be null or empty"); + Assert.notNull(query, "Query must not be null"); Assert.notNull(queryConfiguration, "JpaQueryConfiguration must not be null"); this.valueExpressionDelegate = queryConfiguration.getValueExpressionDelegate(); this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters()); - this.query = ExpressionBasedStringQuery.create(queryString, method, queryConfiguration); + + this.query = TemplatedQuery.create(query, method.getEntityInformation(), queryConfiguration); this.countQuery = Lazy.of(() -> { - if (StringUtils.hasText(countQueryString)) { - return ExpressionBasedStringQuery.create(countQueryString, method, queryConfiguration); + if (countQuery != null) { + return TemplatedQuery.create(countQuery, method.getEntityInformation(), queryConfiguration); } return this.query.deriveCountQuery(method.getCountQueryProjection()); @@ -114,14 +131,18 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { "JDBC style parameters (?) are not supported for JPA queries"); } + private DeclaredQuery createQuery(String queryString, boolean nativeQuery) { + return nativeQuery ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString); + } + @Override public Query doCreateQuery(JpaParametersParameterAccessor accessor) { Sort sort = accessor.getSort(); ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); ReturnedType returnedType = getReturnedType(processor); - String sortedQueryString = getSortedQueryString(sort, returnedType); - Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), returnedType); + QueryProvider sortedQuery = getSortedQuery(sort, returnedType); + Query query = createJpaQuery(sortedQuery, sort, accessor.getPageable(), returnedType); // it is ok to reuse the binding contained in the ParameterBinder, although we create a new query String because the // parameters in the query do not change. @@ -212,7 +233,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { return createBinder(query); } - protected ParameterBinder createBinder(IntrospectedQuery query) { + protected ParameterBinder createBinder(ParametrizedQuery query) { return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, valueExpressionDelegate, valueExpressionContextProvider); } @@ -245,7 +266,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { /** * @return the countQuery */ - public IntrospectedQuery getCountQuery() { + public ParametrizedQuery getCountQuery() { return countQuery.get(); } @@ -253,11 +274,11 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery} * type. */ - protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable, + protected Query createJpaQuery(QueryProvider query, Sort sort, @Nullable Pageable pageable, ReturnedType returnedType) { EntityManager em = getEntityManager(); - String queryToUse = potentiallyRewriteQuery(queryString, sort, pageable); + String queryToUse = potentiallyRewriteQuery(query.getQueryString(), sort, pageable); if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) { return em.createQuery(queryToUse); @@ -286,8 +307,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { : queryRewriter.rewrite(originalQuery, sort); } - String applySorting(CachableQuery cachableQuery) { - return cachableQuery.getDeclaredQuery().getQueryEnhancer() + QueryProvider applySorting(CachableQuery cachableQuery) { + return cachableQuery.getDeclaredQuery() .rewrite(new DefaultQueryRewriteInformation(cachableQuery.getSort(), cachableQuery.getReturnedType())); } @@ -295,7 +316,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * Query Sort Rewriter interface. */ interface QuerySortRewriter { - String getSorted(StringQuery query, Sort sort, ReturnedType returnedType); + QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType); } /** @@ -305,28 +326,28 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { INSTANCE; - public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { - return query.getQueryEnhancer().rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); + public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) { + return query.rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); } } static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter { - private volatile @Nullable String cachedQueryString; + private volatile @Nullable QueryProvider cachedQuery; - public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { + public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) { if (sort.isSorted()) { throw new UnsupportedOperationException("NoOpQueryCache does not support sorting"); } - String cachedQueryString = this.cachedQueryString; - if (cachedQueryString == null) { - this.cachedQueryString = cachedQueryString = query.getQueryEnhancer() + QueryProvider cachedQuery = this.cachedQuery; + if (cachedQuery == null) { + this.cachedQuery = cachedQuery = query .rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); } - return cachedQueryString; + return cachedQuery; } } @@ -335,22 +356,22 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { */ class CachingQuerySortRewriter implements QuerySortRewriter { - private final ConcurrentLruCache queryCache = new ConcurrentLruCache<>(16, + private final ConcurrentLruCache queryCache = new ConcurrentLruCache<>(16, AbstractStringBasedJpaQuery.this::applySorting); - private volatile @Nullable String cachedQueryString; + private volatile @Nullable QueryProvider cachedQuery; @Override - public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { + public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) { if (sort.isUnsorted()) { - String cachedQueryString = this.cachedQueryString; - if (cachedQueryString == null) { - this.cachedQueryString = cachedQueryString = queryCache.get(new CachableQuery(query, sort, returnedType)); + QueryProvider cachedQuery = this.cachedQuery; + if (cachedQuery == null) { + this.cachedQuery = cachedQuery = queryCache.get(new CachableQuery(query, sort, returnedType)); } - return cachedQueryString; + return cachedQuery; } return queryCache.get(new CachableQuery(query, sort, returnedType)); @@ -366,12 +387,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { */ static class CachableQuery { - private final StringQuery query; + private final EntityQuery query; private final String queryString; private final Sort sort; private final ReturnedType returnedType; - CachableQuery(StringQuery query, Sort sort, ReturnedType returnedType) { + CachableQuery(EntityQuery query, Sort sort, ReturnedType returnedType) { this.query = query; this.queryString = query.getQueryString(); @@ -379,7 +400,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { this.returnedType = returnedType; } - StringQuery getDeclaredQuery() { + EntityQuery getDeclaredQuery() { return query; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java deleted file mode 100644 index 66e95a93c..000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.jpa.repository.query; - -import java.util.Collections; -import java.util.List; - - -/** - * @author Christoph Strobl - */ -final class BindableQuery implements DeclaredQuery { - - private final DeclaredQuery source; - private final String bindableQueryString; - private final List bindings; - private final boolean usesJdbcStyleParameters; - - public BindableQuery(DeclaredQuery source, String bindableQueryString, List bindings, boolean usesJdbcStyleParameters) { - this.source = source; - this.bindableQueryString = bindableQueryString; - this.bindings = bindings; - this.usesJdbcStyleParameters = usesJdbcStyleParameters; - } - - @Override - public boolean isNativeQuery() { - return source.isNativeQuery(); - } - - boolean hasBindings() { - return !bindings.isEmpty(); - } - - boolean usesJdbcStyleParameters() { - return usesJdbcStyleParameters; - } - - @Override - public String getQueryString() { - return bindableQueryString; - } - - public BindableQuery unifyBindings(BindableQuery comparisonQuery) { - if (comparisonQuery.hasBindings() && !comparisonQuery.bindings.equals(this.bindings)) { - return new BindableQuery(source, bindableQueryString, comparisonQuery.bindings, usesJdbcStyleParameters); - } - return this; - } - - public List getBindings() { - return Collections.unmodifiableList(bindings); - } -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java new file mode 100644 index 000000000..2f6db9c5f --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java @@ -0,0 +1,148 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import org.springframework.util.ObjectUtils; + +/** + * Utility class encapsulating {@code DeclaredQuery} implementations. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 4.0 + */ +class DeclaredQueries { + + static final class JpqlQuery implements DeclaredQuery { + + private final String jpql; + + JpqlQuery(String jpql) { + this.jpql = jpql; + } + + @Override + public boolean isNative() { + return false; + } + + @Override + public String getQueryString() { + return jpql; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof JpqlQuery jpqlQuery)) { + return false; + } + return ObjectUtils.nullSafeEquals(jpql, jpqlQuery.jpql); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(jpql); + } + + @Override + public String toString() { + return "JPQL[" + jpql + "]"; + } + + } + + static final class NativeQuery implements DeclaredQuery { + + private final String sql; + + NativeQuery(String sql) { + this.sql = sql; + } + + @Override + public boolean isNative() { + return true; + } + + @Override + public String getQueryString() { + return sql; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof NativeQuery that)) { + return false; + } + return ObjectUtils.nullSafeEquals(sql, that.sql); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(sql); + } + + @Override + public String toString() { + return "Native[" + sql + "]"; + } + + } + + /** + * A rewritten {@link DeclaredQuery} holding a reference to its original query. + */ + static class RewrittenQuery implements DeclaredQuery { + + private final DeclaredQuery source; + private final String queryString; + + public RewrittenQuery(DeclaredQuery source, String queryString) { + this.source = source; + this.queryString = queryString; + } + + @Override + public boolean isNative() { + return source.isNative(); + } + + @Override + public String getQueryString() { + return queryString; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RewrittenQuery that)) { + return false; + } + return ObjectUtils.nullSafeEquals(queryString, that.queryString); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(queryString); + } + + @Override + public String toString() { + return isNative() ? "Rewritten Native[" + queryString + "]" : "Rewritten JPQL[" + queryString + "]"; + } + + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java index 152c40c38..2cea734db 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java @@ -17,13 +17,17 @@ package org.springframework.data.jpa.repository.query; /** * Interface defining the contract to represent a declared query. + *

+ * Declared queries consist of a query string and a flag whether the query is a native (SQL) one or a JPQL query. + * Queries can be rewritten to contain a different query string (i.e. count query derivation, sorting, projection + * updates) while retaining their {@link #isNative() native} flag. * * @author Jens Schauder * @author Diego Krupitza * @author Mark Paluch * @since 2.0.3 */ -public interface DeclaredQuery extends StructuredQuery { +public interface DeclaredQuery extends QueryProvider { /** * Creates a DeclaredQuery for a JPQL query. @@ -32,7 +36,7 @@ public interface DeclaredQuery extends StructuredQuery { * @return new instance of {@link DeclaredQuery}. */ static DeclaredQuery jpqlQuery(String jpql) { - return new JpqlQuery(jpql); + return new DeclaredQueries.JpqlQuery(jpql); } /** @@ -42,13 +46,40 @@ public interface DeclaredQuery extends StructuredQuery { * @return new instance of {@link DeclaredQuery}. */ static DeclaredQuery nativeQuery(String sql) { - return new NativeQuery(sql); + return new DeclaredQueries.NativeQuery(sql); } /** * Return whether the query is a native query of not. * - * @return true if native query otherwise false + * @return {@literal true} if native query; {@literal false} if it is a JPQL query. */ - boolean isNativeQuery(); + boolean isNative(); + + /** + * Return whether the query is a JPQL query of not. + * + * @return {@literal true} if JPQL query; {@literal false} if it is a native query. + * @since 4.0 + */ + default boolean isJpql() { + return !isNative(); + } + + /** + * Rewrite a query string using a new query string retaining its source and {@link #isNative() native} flag. + * + * @param newQueryString the new query string. + * @return the rewritten {@link DeclaredQuery}. + * @since 4.0 + */ + default DeclaredQuery rewrite(String newQueryString) { + + if (getQueryString().equals(newQueryString)) { + return this; + } + + return new DeclaredQueries.RewrittenQuery(this, newQueryString); + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java new file mode 100644 index 000000000..bde36d153 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java @@ -0,0 +1,159 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import java.util.List; + +import org.jspecify.annotations.Nullable; + +/** + * Encapsulation of a JPA query string, typically returning entities or DTOs. Provides access to parameter bindings. + *

+ * The internal {@link PreprocessedQuery query string} is cleaned from decorated parameters like {@literal %:lastname%} + * and the matching bindings take care of applying the decorations in the {@link ParameterBinding#prepare(Object)} + * method. Note that this class also handles replacing SpEL expressions with synthetic bind parameters. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Oliver Wehrens + * @author Mark Paluch + * @author Jens Schauder + * @author Diego Krupitza + * @author Greg Turnquist + * @author Yuriy Tsarkov + * @since 4.0 + */ +class DefaultEntityQuery implements EntityQuery, DeclaredQuery { + + private final PreprocessedQuery query; + private final QueryEnhancer queryEnhancer; + + DefaultEntityQuery(PreprocessedQuery query, QueryEnhancerFactory queryEnhancerFactory) { + this.query = query; + this.queryEnhancer = queryEnhancerFactory.create(query); + } + + @Override + public boolean isNative() { + return query.isNative(); + } + + @Override + public String getQueryString() { + return query.getQueryString(); + } + + /** + * Returns whether we have found some like bindings. + */ + @Override + public boolean hasParameterBindings() { + return this.query.hasBindings(); + } + + @Override + public boolean usesJdbcStyleParameters() { + return query.usesJdbcStyleParameters(); + } + + @Override + public boolean hasNamedParameter() { + return query.hasNamedBindings(); + } + + @Override + public List getParameterBindings() { + return this.query.getBindings(); + } + + @Override + public boolean hasConstructorExpression() { + return queryEnhancer.hasConstructorExpression(); + } + + @Override + public boolean isDefaultProjection() { + return queryEnhancer.getProjection().equalsIgnoreCase(getAlias()); + } + + @Nullable + String getAlias() { + return queryEnhancer.detectAlias(); + } + + @Override + public boolean usesPaging() { + return query.containsPageableInSpel(); + } + + String getProjection() { + return this.queryEnhancer.getProjection(); + } + + @Override + public ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection) { + return new SimpleParametrizedQuery(this.query.rewrite(queryEnhancer.createCountQueryFor(countQueryProjection))); + } + + @Override + public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation) { + return this.query.rewrite(queryEnhancer.rewrite(rewriteInformation)); + } + + @Override + public String toString() { + return "EntityQuery[" + getQueryString() + ", " + getParameterBindings() + ']'; + } + + /** + * Simple {@link ParametrizedQuery} variant forwarding to {@link PreprocessedQuery}. + */ + static class SimpleParametrizedQuery implements ParametrizedQuery { + + private final PreprocessedQuery query; + + SimpleParametrizedQuery(PreprocessedQuery query) { + this.query = query; + } + + @Override + public String getQueryString() { + return query.getQueryString(); + } + + @Override + public boolean hasParameterBindings() { + return query.hasBindings(); + } + + @Override + public boolean usesJdbcStyleParameters() { + return query.usesJdbcStyleParameters(); + } + + @Override + public boolean hasNamedParameter() { + return query.hasNamedBindings(); + } + + @Override + public List getParameterBindings() { + return query.getBindings(); + } + + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java index 3d4aba285..456c3139b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java @@ -15,10 +15,6 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.Set; - -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; /** @@ -27,30 +23,18 @@ import org.jspecify.annotations.Nullable; * @author Diego Krupitza * @since 2.7.0 */ -public class DefaultQueryEnhancer implements QueryEnhancer { +class DefaultQueryEnhancer implements QueryEnhancer { - private final StructuredQuery query; + private final QueryProvider query; private final boolean hasConstructorExpression; private final @Nullable String alias; private final String projection; - private final Set joinAliases; - public DefaultQueryEnhancer(StructuredQuery query) { + public DefaultQueryEnhancer(QueryProvider query) { this.query = query; this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString()); this.alias = QueryUtils.detectAlias(query.getQueryString()); this.projection = QueryUtils.getProjection(this.query.getQueryString()); - this.joinAliases = QueryUtils.getOuterJoinAliases(this.query.getQueryString()); - } - - @Override - public String applySorting(Sort sort) { - return QueryUtils.applySorting(this.query.getQueryString(), sort, this.alias); - } - - @Override - public String applySorting(Sort sort, @Nullable String alias) { - return QueryUtils.applySorting(this.query.getQueryString(), sort, alias); } @Override @@ -61,7 +45,7 @@ public class DefaultQueryEnhancer implements QueryEnhancer { @Override public String createCountQueryFor(@Nullable String countProjection) { - boolean nativeQuery = this.query instanceof DeclaredQuery dc ? dc.isNativeQuery() : true; + boolean nativeQuery = this.query instanceof DeclaredQuery dc ? dc.isNative() : true; return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, nativeQuery); } @@ -81,12 +65,8 @@ public class DefaultQueryEnhancer implements QueryEnhancer { } @Override - public Set getJoinAliases() { - return this.joinAliases; - } - - @Override - public StructuredQuery getQuery() { + public QueryProvider getQuery() { return this.query; } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java index ec92ee81c..a0ef2363b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java @@ -18,21 +18,30 @@ package org.springframework.data.jpa.repository.query; import java.util.Collections; import java.util.List; -import org.springframework.data.domain.Sort; import org.jspecify.annotations.Nullable; /** - * NULL-Object pattern implementation for {@link IntrospectedQuery}. + * NULL-Object pattern implementation for {@link ParametrizedQuery}. * * @author Jens Schauder + * @author Mark Paluch * @since 2.0.3 */ -class EmptyIntrospectedQuery implements EntityQuery { +enum EmptyIntrospectedQuery implements EntityQuery { - /** - * An implementation implementing the NULL-Object pattern for situations where there is no query. - */ - static final EntityQuery EMPTY_QUERY = new EmptyIntrospectedQuery(); + INSTANCE; + + EmptyIntrospectedQuery() {} + + @Override + public boolean hasParameterBindings() { + return false; + } + + @Override + public boolean usesJdbcStyleParameters() { + return false; + } @Override public boolean hasNamedParameter() { @@ -40,19 +49,14 @@ class EmptyIntrospectedQuery implements EntityQuery { } @Override - public String getQueryString() { - return ""; + public List getParameterBindings() { + return Collections.emptyList(); } public @Nullable String getAlias() { return null; } - @Override - public boolean isNativeQuery() { - return false; - } - @Override public boolean hasConstructorExpression() { return false; @@ -69,27 +73,18 @@ class EmptyIntrospectedQuery implements EntityQuery { } @Override - public List getParameterBindings() { - return Collections.emptyList(); + public ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection) { + return INSTANCE; } @Override - public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) { - return EMPTY_QUERY; + public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation) { + return this; } @Override - public String applySorting(Sort sort) { - return ""; + public String toString() { + return ""; } - @Override - public boolean usesJdbcStyleParameters() { - return false; - } - - @Override - public DeclaredQuery getDeclaredQuery() { - return DeclaredQuery.nativeQuery(""); - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java index b959d3810..b28fa9f10 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java @@ -15,61 +15,34 @@ */ package org.springframework.data.jpa.repository.query; -import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; +import org.jspecify.annotations.Nullable; /** - * A wrapper for a String representation of a query offering information about the query. + * An extension to {@link ParametrizedQuery} exposing query information about its inner structure such as whether + * constructor expressions (JPQL) are used or the default projection is used. + *

+ * Entity Queries support derivation of {@link #deriveCountQuery(String) count queries} from the original query. They + * also can be used to rewrite the query using sorting and projection selection. * * @author Jens Schauder * @author Diego Krupitza - * @since 2.0.3 + * @since 4.0 */ -interface EntityQuery extends IntrospectedQuery { +interface EntityQuery extends ParametrizedQuery { /** - * Creates a DeclaredQuery for a JPQL query. + * Create a new {@link EntityQuery} given {@link DeclaredQuery} and {@link QueryEnhancerSelector}. * - * @param query the JPQL query string. - * @return + * @param query must not be {@literal null}. + * @param selector must not be {@literal null}. + * @return a new {@link EntityQuery}. */ - static EntityQuery introspectJpql(String query, QueryEnhancerFactory queryEnhancer) { - return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY - : new StringQuery(query, false, queryEnhancer, parameterBindings -> {}); - } + static EntityQuery create(DeclaredQuery query, QueryEnhancerSelector selector) { - /** - * Creates a DeclaredQuery for a JPQL query. - * - * @param query the JPQL query string. - * @return - */ - static EntityQuery introspectJpql(String query, QueryEnhancerSelector selector) { - return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY - : new StringQuery(query, false, selector, parameterBindings -> {}); - } + PreprocessedQuery preparsed = PreprocessedQuery.parse(query); + QueryEnhancerFactory enhancerFactory = selector.select(preparsed); - /** - * Creates a DeclaredQuery for a native query. - * - * @param query the native query string. - * @return - */ - static EntityQuery introspectNativeQuery(String query, QueryEnhancerFactory queryEnhancer) { - return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY - : new StringQuery(query, true, queryEnhancer, parameterBindings -> {}); - } - - /** - * Creates a DeclaredQuery for a native query. - * - * @param query the native query string. - * @return - */ - static EntityQuery introspectNativeQuery(String query, QueryEnhancerSelector selector) { - return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY - : new StringQuery(query, true, selector, parameterBindings -> {}); + return new DefaultEntityQuery(preparsed, enhancerFactory); } /** @@ -84,18 +57,6 @@ interface EntityQuery extends IntrospectedQuery { */ boolean isDefaultProjection(); - /** - * Creates a new {@literal IntrospectedQuery} representing a count query, i.e. a query returning the number of rows to - * be expected from the original query, either derived from the query wrapped by this instance or from the information - * passed as arguments. - * - * @param countQueryProjection an optional return type for the query. - * @return a new {@literal IntrospectedQuery} instance. - */ - IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection); - - String applySorting(Sort sort); - /** * @return whether paging is implemented in the query itself, e.g. using SpEL expressions. * @since 2.0.6 @@ -104,4 +65,24 @@ interface EntityQuery extends IntrospectedQuery { return false; } + /** + * Creates a new {@literal IntrospectedQuery} representing a count query, i.e. a query returning the number of rows to + * be expected from the original query, either derived from the query wrapped by this instance or from the information + * passed as arguments. + * + * @param countQueryProjection an optional return type for the query. + * @return a new {@literal IntrospectedQuery} instance. + */ + ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection); + + /** + * Rewrite the query using the given + * {@link org.springframework.data.jpa.repository.query.QueryEnhancer.QueryRewriteInformation} into a sorted query or + * using a different projection. The rewritten query retains parameter binding characteristics. + * + * @param rewriteInformation query rewrite information (sorting, projection) to use. + * @return the rewritten query. + */ + QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation); + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java index d1fab1032..1733df96e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java @@ -74,7 +74,7 @@ import org.springframework.util.StringUtils; */ public class JSqlParserQueryEnhancer implements QueryEnhancer { - private final StructuredQuery query; + private final QueryProvider query; private final Statement statement; private final ParsedType parsedType; private final boolean hasConstructorExpression; @@ -87,7 +87,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { /** * @param query the query we want to enhance. Must not be {@literal null}. */ - public JSqlParserQueryEnhancer(StructuredQuery query) { + public JSqlParserQueryEnhancer(QueryProvider query) { this.query = query; this.statement = parseStatement(query.getQueryString(), Statement.class); @@ -329,35 +329,20 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { return this.projection; } - @Override - public Set getJoinAliases() { - return joinAliases; - } - public Set getSelectionAliases() { return selectAliases; } @Override - public StructuredQuery getQuery() { + public QueryProvider getQuery() { return this.query; } - @Override - public String applySorting(Sort sort) { - return doApplySorting(sort, detectAlias()); - } - @Override public String rewrite(QueryRewriteInformation rewriteInformation) { return doApplySorting(rewriteInformation.getSort(), primaryAlias); } - @Override - public String applySorting(Sort sort, @Nullable String alias) { - return doApplySorting(sort, alias); - } - private String doApplySorting(Sort sort, @Nullable String alias) { String queryString = query.getQueryString(); Assert.hasText(queryString, "Query must not be null or empty"); @@ -410,8 +395,8 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { this.query::getQueryString); } - private static String createCountQueryFor(StructuredQuery query, PlainSelect selectBody, - @Nullable String countProjection, @Nullable String primaryAlias) { + private static String createCountQueryFor(PlainSelect selectBody, @Nullable String countProjection, + @Nullable String primaryAlias) { // remove order by selectBody.setOrderByElements(null); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java index 7bce8dc8f..788c977f2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java @@ -54,4 +54,5 @@ public class JpaQueryConfiguration { public ValueExpressionDelegate getValueExpressionDelegate() { return valueExpressionDelegate; } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index ff4b6efb7..04d134c0a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java @@ -16,7 +16,6 @@ package org.springframework.data.jpa.repository.query; import java.util.List; -import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; @@ -36,7 +35,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.util.Assert; /** * Implementation of {@link QueryEnhancer} to enhance JPA queries using ANTLR parsers. @@ -55,11 +53,11 @@ class JpaQueryEnhancer implements QueryEnhancer { private final Q queryInformation; private final String projection; private final SortedQueryRewriteFunction sortFunction; - private final BiFunction> countQueryFunction; + private final BiFunction<@Nullable String, Q, ParseTreeVisitor> countQueryFunction; JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector, SortedQueryRewriteFunction sortFunction, - BiFunction> countQueryFunction) { + BiFunction<@Nullable String, Q, ParseTreeVisitor> countQueryFunction) { this.context = context; this.sortFunction = sortFunction; @@ -142,7 +140,7 @@ class JpaQueryEnhancer implements QueryEnhancer { } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using JPQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link ParametrizedQuery} using JPQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using JPQL. @@ -152,7 +150,7 @@ class JpaQueryEnhancer implements QueryEnhancer { } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using HQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link ParametrizedQuery} using HQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using HQL. @@ -162,7 +160,7 @@ class JpaQueryEnhancer implements QueryEnhancer { } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using EQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link ParametrizedQuery} using EQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using EQL. @@ -197,8 +195,7 @@ class JpaQueryEnhancer implements QueryEnhancer { } /** - * Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can - * already find the alias when generating sorted and count queries, this is mainly to serve test cases. + * Resolves the alias for the entity in the FROM clause from the JPA query. */ @Override public @Nullable String detectAlias() { @@ -206,24 +203,13 @@ class JpaQueryEnhancer implements QueryEnhancer { } /** - * Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when - * generating sorted and count queries, this is mainly to serve test cases. + * Looks up the projection of the JPA query. */ @Override public String getProjection() { return this.projection; } - /** - * Since the parser can already fully transform sorted and count queries by itself, this is a placeholder method. - * - * @return empty set - */ - @Override - public Set getJoinAliases() { - return Set.of(); - } - /** * Look up the {@link DeclaredQuery} from the query parser. */ @@ -232,17 +218,6 @@ class JpaQueryEnhancer implements QueryEnhancer { throw new UnsupportedOperationException(); } - /** - * Adds an {@literal order by} clause to the JPA query. - * - * @param sort the sort specification to apply. - * @return - */ - @Override - public String applySorting(Sort sort) { - return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, this.queryInformation, null).visit(context)); - } - @Override public String rewrite(QueryRewriteInformation rewriteInformation) { return QueryRenderer.TokenRenderer.render( @@ -250,28 +225,6 @@ class JpaQueryEnhancer implements QueryEnhancer { .visit(context)); } - /** - * Because the parser can find the alias of the FROM clause, there is no need to "find it" in advance. - * - * @param sort the sort specification to apply. - * @param alias IGNORED - * @return - */ - @Override - public String applySorting(Sort sort, @Nullable String alias) { - return applySorting(sort); - } - - /** - * Creates a count query from the original query, with no count projection. - * - * @return Guaranteed to be not {@literal null}; - */ - @Override - public String createCountQueryFor() { - return createCountQueryFor(null); - } - /** * Create a count query from the original query, with potential custom projection. * 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 b032000ba..719e838fe 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 @@ -32,7 +32,6 @@ import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -151,20 +150,22 @@ public final class JpaQueryLookupStrategy { return createProcedureQuery(method, em); } - if (StringUtils.hasText(method.getAnnotatedQuery())) { + if (method.hasAnnotatedQuery()) { if (method.hasAnnotatedQueryName()) { LOG.warn(String.format( "Query method %s is annotated with both, a query and a query name; Using the declared query", method)); } - return createStringQuery(method, em, method.getRequiredAnnotatedQuery(), + return createStringQuery(method, em, method.getRequiredDeclaredQuery(), getCountQuery(method, namedQueries, em), configuration); } String name = method.getNamedQueryName(); + if (namedQueries.hasQuery(name)) { - return createStringQuery(method, em, namedQueries.getQuery(name), getCountQuery(method, namedQueries, em), + return createStringQuery(method, em, method.getDeclaredQuery(namedQueries.getQuery(name)), + getCountQuery(method, namedQueries, em), configuration); } @@ -173,7 +174,15 @@ public final class JpaQueryLookupStrategy { return query != null ? query : NO_QUERY; } - private @Nullable String getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) { + private @Nullable DeclaredQuery getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) { + + String query = doGetCountQuery(method, namedQueries, em); + + return StringUtils.hasText(query) ? method.getDeclaredQuery(query) : null; + } + + private static @Nullable String doGetCountQuery(JpaQueryMethod method, NamedQueries namedQueries, + EntityManager em) { if (StringUtils.hasText(method.getCountQuery())) { return method.getCountQuery(); @@ -203,20 +212,20 @@ public final class JpaQueryLookupStrategy { * * @param method must not be {@literal null}. * @param em must not be {@literal null}. - * @param queryString must not be {@literal null}. - * @param countQueryString must not be {@literal null}. + * @param query must not be {@literal null}. + * @param countQuery can be {@literal null} if not defined. * @param configuration must not be {@literal null}. * @return */ - static AbstractJpaQuery createStringQuery(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, JpaQueryConfiguration configuration) { + static AbstractJpaQuery createStringQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query, + @Nullable DeclaredQuery countQuery, JpaQueryConfiguration configuration) { 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, configuration) - : new SimpleJpaQuery(method, em, queryString, countQueryString, configuration); + return method.isNativeQuery() ? new NativeJpaQuery(method, em, query, countQuery, configuration) + : new SimpleJpaQuery(method, em, query, countQuery, configuration); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index 25f50b9f2..9a702d646 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java @@ -27,9 +27,9 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; -import org.springframework.core.annotation.AnnotatedElementUtils; - import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.EntityGraph; @@ -295,6 +295,13 @@ public class JpaQueryMethod extends QueryMethod { return metaAttributes; } + /** + * @return {@code true} if this method is annotated with {@code @Query(value=…)}. + */ + boolean hasAnnotatedQuery() { + return StringUtils.hasText(getAnnotationValue("value", String.class)); + } + /** * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found * nor the attribute was specified. @@ -333,6 +340,25 @@ public class JpaQueryMethod extends QueryMethod { throw new IllegalStateException(String.format("No annotated query found for query method %s", getName())); } + /** + * Returns the required {@link DeclaredQuery} from a {@link Query} annotation or throws {@link IllegalStateException} + * if neither the annotation found nor the attribute was specified. + * + * @return + * @throws IllegalStateException if no {@link Query} annotation is present or the query is empty. + * @since 4.0 + */ + public DeclaredQuery getRequiredDeclaredQuery() throws IllegalStateException { + + String query = getAnnotatedQuery(); + + if (query != null) { + return getDeclaredQuery(query); + } + + throw new IllegalStateException(String.format("No annotated query found for query method %s", getName())); + } + /** * Returns the countQuery string declared in a {@link Query} annotation or {@literal null} if neither the annotation * found nor the attribute was specified. @@ -345,6 +371,19 @@ public class JpaQueryMethod extends QueryMethod { return StringUtils.hasText(countQuery) ? countQuery : null; } + /** + * Returns the {@link DeclaredQuery declared count query} from a {@link Query} annotation or {@literal null} if + * neither the annotation found nor the attribute was specified. + * + * @return + * @since 4.0 + */ + public @Nullable DeclaredQuery getDeclaredCountQuery() { + + String countQuery = getAnnotationValue("countQuery", String.class); + return StringUtils.hasText(countQuery) ? getDeclaredQuery(countQuery) : null; + } + /** * Returns the count query projection string declared in a {@link Query} annotation or {@literal null} if neither the * annotation found nor the attribute was specified. @@ -368,6 +407,17 @@ public class JpaQueryMethod extends QueryMethod { return this.isNativeQuery.get(); } + /** + * Utility method that returns a {@link DeclaredQuery} object for the given {@code queryString}. + * + * @param query the query string to wrap. + * @return a {@link DeclaredQuery} object for the given {@code queryString}. + * @since 4.0 + */ + DeclaredQuery getDeclaredQuery(String query) { + return isNativeQuery() ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); + } + @Override public String getNamedQueryName() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index b81820c6f..de26c392b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -80,7 +80,7 @@ final class NamedQuery extends AbstractJpaQuery { this.namedCountQueryIsPresent = hasNamedQuery(em, countQueryName); - Query query = em.createNamedQuery(queryName); + Query namedQuery = em.createNamedQuery(queryName); boolean weNeedToCreateCountQuery = !namedCountQueryIsPresent && method.getParameters().hasLimitingParameters(); boolean cantExtractQuery = !extractor.canExtractQuery(); @@ -94,14 +94,17 @@ final class NamedQuery extends AbstractJpaQuery { method, method.isNativeQuery() ? "NativeQuery" : "Query")); } - String queryString = extractor.extractQueryString(query); + String queryString = extractor.extractQueryString(namedQuery); // TODO: What is queryString is null? - if (method.isNativeQuery() || (query != null && query.toString().contains("NativeQuery"))) { - this.entityQuery = Lazy.of(() -> EntityQuery.introspectNativeQuery(queryString, selector)); + DeclaredQuery declaredQuery; + if (method.isNativeQuery() || (namedQuery != null && namedQuery.toString().contains("NativeQuery"))) { + declaredQuery = DeclaredQuery.nativeQuery(queryString); } else { - this.entityQuery = Lazy.of(() -> EntityQuery.introspectJpql(queryString, selector)); + declaredQuery = DeclaredQuery.jpqlQuery(queryString); } + + this.entityQuery = Lazy.of(() -> EntityQuery.create(declaredQuery, selector)); } /** 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 4c2fefe23..35045c5e2 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 @@ -19,16 +19,15 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import jakarta.persistence.Tuple; -import org.springframework.core.annotation.MergedAnnotation; - import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.ObjectUtils; /** @@ -57,23 +56,45 @@ class NativeJpaQuery extends AbstractStringBasedJpaQuery { * @param countQueryString must not be {@literal null} or empty. * @param queryConfiguration must not be {@literal null}. */ - public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, + NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) { super(method, em, queryString, countQueryString, queryConfiguration); MergedAnnotations annotations = MergedAnnotations.from(method.getMethod()); MergedAnnotation annotation = annotations.get(NativeQuery.class); - this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null; + this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null; + this.queryForEntity = getQueryMethod().isQueryForEntity(); + } + + /** + * Creates a new {@link NativeJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}. + * + * @param method must not be {@literal null}. + * @param em must not be {@literal null}. + * @param query must not be {@literal null} . + * @param countQuery can be {@literal null} if not defined. + * @param queryConfiguration must not be {@literal null}. + */ + public NativeJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query, + @Nullable DeclaredQuery countQuery, JpaQueryConfiguration queryConfiguration) { + + super(method, em, query, countQuery, queryConfiguration); + + MergedAnnotations annotations = MergedAnnotations.from(method.getMethod()); + MergedAnnotation annotation = annotations.get(NativeQuery.class); + + this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null; this.queryForEntity = getQueryMethod().isQueryForEntity(); } @Override - protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable, ReturnedType returnedType) { + protected Query createJpaQuery(QueryProvider declaredQuery, Sort sort, @Nullable Pageable pageable, + ReturnedType returnedType) { EntityManager em = getEntityManager(); - String query = potentiallyRewriteQuery(queryString, sort, pageable); + String query = potentiallyRewriteQuery(declaredQuery.getQueryString(), sort, pageable); if (!ObjectUtils.isEmpty(sqlResultSetMapping)) { return em.createNativeQuery(query, sqlResultSetMapping); 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 fc34606f4..00aef2619 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 @@ -78,13 +78,13 @@ class ParameterBinderFactory { * query in order to ensure that all query parameters are bound. * * @param parameters method parameters that are available for binding, must not be {@literal null}. - * @param query the {@link StringQuery} the binders shall be created for, must not be {@literal null}. + * @param query the {@link DefaultEntityQuery} the binders shall be created for, must not be {@literal null}. * @param parser must not be {@literal null}. * @param evaluationContextProvider must not be {@literal null}. * @return a {@link ParameterBinder} that can assign values for the method parameters to query parameters of a * {@link jakarta.persistence.Query} while processing SpEL expressions where applicable. */ - static ParameterBinder createQueryAwareBinder(JpaParameters parameters, IntrospectedQuery query, + static ParameterBinder createQueryAwareBinder(JpaParameters parameters, ParametrizedQuery query, ValueExpressionParser parser, ValueEvaluationContextProvider evaluationContextProvider) { Assert.notNull(parameters, "JpaParameters must not be null"); @@ -126,11 +126,11 @@ class ParameterBinderFactory { private static Iterable createSetters(List parameterBindings, QueryParameterSetterFactory... factories) { - return createSetters(parameterBindings, EmptyIntrospectedQuery.EMPTY_QUERY, factories); + return createSetters(parameterBindings, EmptyIntrospectedQuery.INSTANCE, factories); } private static Iterable createSetters(List parameterBindings, - IntrospectedQuery query, QueryParameterSetterFactory... strategies) { + ParametrizedQuery query, QueryParameterSetterFactory... strategies) { List setters = new ArrayList<>(parameterBindings.size()); for (ParameterBinding parameterBinding : parameterBindings) { @@ -141,7 +141,7 @@ class ParameterBinderFactory { } private static QueryParameterSetter createQueryParameterSetter(ParameterBinding binding, - QueryParameterSetterFactory[] strategies, IntrospectedQuery query) { + QueryParameterSetterFactory[] strategies, ParametrizedQuery query) { for (QueryParameterSetterFactory strategy : strategies) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java similarity index 64% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java index 4a29bce6c..85a314127 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java @@ -18,34 +18,24 @@ package org.springframework.data.jpa.repository.query; import java.util.List; /** - * A wrapper for a String representation of a query offering information about the query. + * A parsed and structured representation of a query providing introspection details about parameter bindings. + *

+ * Structured queries can be either created from {@link EntityQuery} introspection or through + * {@link EntityQuery#deriveCountQuery(String) count query derivation}. * * @author Jens Schauder * @author Diego Krupitza - * @since 2.0.3 + * @since 4.0 + * @see EntityQuery + * @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector) + * @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration) */ -interface IntrospectedQuery extends StructuredQuery { - - DeclaredQuery getDeclaredQuery(); - - default String getQueryString() { - return getDeclaredQuery().getQueryString(); - } +interface ParametrizedQuery extends QueryProvider { /** - * @return whether the underlying query has at least one named parameter. + * @return whether the underlying query has at least one parameter. */ - boolean hasNamedParameter(); - - /** - * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query. - */ - boolean isDefaultProjection(); - - /** - * Returns the {@link ParameterBinding}s registered. - */ - List getParameterBindings(); + boolean hasParameterBindings(); /** * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or @@ -56,4 +46,14 @@ interface IntrospectedQuery extends StructuredQuery { */ boolean usesJdbcStyleParameters(); + /** + * @return whether the underlying query has at least one named parameter. + */ + boolean hasNamedParameter(); + + /** + * @return the registered {@link ParameterBinding}s. + */ + List getParameterBindings(); + } 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/PreprocessedQuery.java similarity index 61% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java index e15bcb2e3..0c5061b52 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/PreprocessedQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2025 the original author or authors. + * Copyright 2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import static java.util.regex.Pattern.*; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -29,18 +30,12 @@ import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.data.domain.Sort; -import org.springframework.data.expression.ValueExpression; - import org.jspecify.annotations.Nullable; + +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.ValueExpressionQueryRewriter; -import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.data.repository.query.parser.Part; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,190 +43,44 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned - * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations - * in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions - * with synthetic bind parameters. + * A pre-parsed query implementing {@link DeclaredQuery} providing information about parameter bindings. + *

+ * Query-preprocessing transforms queries using Spring Data-specific syntax such as {@link TemplatedQuery query + * templating}, extended {@code LIKE} syntax and usage of {@link ValueExpression value expressions} into a syntax that + * is valid for JPA queries (JPQL and native). + *

+ * Preprocessing consists of parsing and rewriting so that no extension elements interfere with downstream parsers. + * However, pre-processing is a lossy procedure because the resulting {@link #getQueryString() query string} only + * contains parameter binding markers and so the original query cannot be restored. Any query derivation must align its + * {@link ParameterBinding parameter bindings} to ensure the derived query uses the same binding semantics instead of + * plain parameters. See {@link ParameterBinding#isCompatibleWith(ParameterBinding)} for further reference. * - * @author Oliver Gierke - * @author Thomas Darimont - * @author Oliver Wehrens + * @author Christoph Strobl * @author Mark Paluch - * @author Jens Schauder - * @author Diego Krupitza - * @author Greg Turnquist - * @author Yuriy Tsarkov + * @since 4.0 */ -class StringQuery implements EntityQuery { +final class PreprocessedQuery implements DeclaredQuery { - private final BindableQuery bindableQuery; + private final DeclaredQuery source; + private final List bindings; + private final boolean usesJdbcStyleParameters; private final boolean containsPageableInSpel; - private final QueryEnhancerFactory queryEnhancerFactory; - private final QueryEnhancer queryEnhancer; - private final boolean hasNamedParameters; + private final boolean hasNamedBindings; - /** - * Creates a new {@link StringQuery} from the given JPQL query. - * - * @param query must not be {@literal null} or empty. - */ - StringQuery(String query, boolean isNative) { - this(query, isNative, QueryEnhancerSelector.DEFAULT_SELECTOR, it -> {}); - } - - /** - * Creates a new {@link StringQuery} from the given JPQL query. - * - * @param query must not be {@literal null} or empty. - */ - StringQuery(String query, boolean isNative, QueryEnhancerFactory factory,Consumer> parameterPostProcessor) { - - Assert.hasText(query, "Query must not be null or empty"); - - this.containsPageableInSpel = query.contains("#pageable"); - this.queryEnhancerFactory = factory; - - DeclaredQuery source = isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); - this.bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); - - parameterPostProcessor.accept(this.bindableQuery.getBindings()); - this.queryEnhancer = factory.create(this.bindableQuery); - this.hasNamedParameters = containsNamedParameter(this.bindableQuery.getBindings()); - } - - /** - * Creates a new {@link StringQuery} from the given JPQL query. - * - * @param query must not be {@literal null} or empty. - */ - StringQuery(String query, boolean isNative, QueryEnhancerSelector selector, Consumer> parameterPostProcessor) { - - Assert.hasText(query, "Query must not be null or empty"); - - this.containsPageableInSpel = query.contains("#pageable"); - DeclaredQuery source = isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); - - this.bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); - - this.queryEnhancerFactory = selector.select(source); - this.queryEnhancer = queryEnhancerFactory.create(this.bindableQuery); - parameterPostProcessor.accept(this.bindableQuery.getBindings()); - this.hasNamedParameters = containsNamedParameter(this.bindableQuery.getBindings()); - } - - /** - * internal copy constructor - * - * @param bindableQuery - * @param factory - * @param enhancer - * @param hasNamedParameters - * @param containsPageableInSpel - */ - private StringQuery(BindableQuery bindableQuery, QueryEnhancerFactory factory, QueryEnhancer enhancer, boolean hasNamedParameters, boolean containsPageableInSpel) { - this.bindableQuery = bindableQuery; - this.queryEnhancerFactory = factory; - this.queryEnhancer = enhancer; - this.hasNamedParameters = hasNamedParameters; + private PreprocessedQuery(DeclaredQuery query, List bindings, boolean usesJdbcStyleParameters, + boolean containsPageableInSpel) { + this.source = query; + this.bindings = bindings; + this.usesJdbcStyleParameters = usesJdbcStyleParameters; this.containsPageableInSpel = containsPageableInSpel; - } - - QueryEnhancer getQueryEnhancer() { - return queryEnhancer; - } - - /** - * Returns whether we have found some like bindings. - */ - boolean hasParameterBindings() { - return this.bindableQuery.hasBindings(); - } - - String getProjection() { - return this.queryEnhancer.getProjection(); - } - - @Override - public String getQueryString() { - return bindableQuery.getQueryString(); - } - - @Override - public List getParameterBindings() { - return this.bindableQuery.getBindings(); - } - - @Override - public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) { - - // need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees - // JPA parameter markers and not the original expressions anymore. - - return new StringQuery(this.queryEnhancer.createCountQueryFor(countQueryProjection), // - this.bindableQuery.isNativeQuery(), queryEnhancerFactory, derivedBindings -> { - - // need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees - // JPA - // parameter markers and not the original expressions anymore. - if (this.hasParameterBindings() && !this.getParameterBindings().equals(derivedBindings)) { - - for (ParameterBinding binding : getParameterBindings()) { - - Predicate identifier = binding::bindsTo; - Predicate notCompatible = Predicate.not(binding::isCompatibleWith); - - // replace incompatible bindings - if ( derivedBindings.removeIf( - it -> identifier.test(it) && notCompatible.test(it))) { - derivedBindings.add(binding); - } - } - } - }); - } - - @Override - public String applySorting(Sort sort) { - return queryEnhancer.applySorting(sort); - } - - @Override - public boolean usesJdbcStyleParameters() { - return bindableQuery.usesJdbcStyleParameters(); - } - - public @Nullable String getAlias() { - return queryEnhancer.detectAlias(); - } - - @Override - public boolean hasConstructorExpression() { - return queryEnhancer.hasConstructorExpression(); - } - - @Override - public boolean isDefaultProjection() { - return getProjection().equalsIgnoreCase(getAlias()); - } - - @Override - public boolean hasNamedParameter() { - return hasNamedParameters; - } - - @Override - public boolean usesPaging() { - return containsPageableInSpel; - } - - @Override - public DeclaredQuery getDeclaredQuery() { - return bindableQuery; + this.hasNamedBindings = containsNamedParameter(bindings); } private static boolean containsNamedParameter(List bindings) { + for (ParameterBinding parameterBinding : bindings) { - if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin() + .isMethodArgument()) { return true; } } @@ -239,77 +88,81 @@ class StringQuery implements EntityQuery { } /** - * Value object to track and allocate used parameter index labels in a query. + * Parse a {@link DeclaredQuery query} into its parametrized form by identifying anonymous, named, indexed and SpEL + * parameters. Query parsing applies special treatment to {@code IN} and {@code LIKE} parameter bindings. + * + * @param declaredQuery the source query to parse. + * @return a parsed {@link PreprocessedQuery}. */ - static class IndexedParameterLabels { + public static PreprocessedQuery parse(DeclaredQuery declaredQuery) { + return ParameterBindingParser.INSTANCE.parse(declaredQuery.getQueryString(), declaredQuery::rewrite, + parameterBindings -> { + }); + } - private final TreeSet usedLabels; - private final boolean sequential; + @Override + public String getQueryString() { + return source.getQueryString(); + } - public IndexedParameterLabels(Set usedLabels) { + @Override + public boolean isNative() { + return source.isNative(); + } - this.usedLabels = usedLabels instanceof TreeSet ts ? ts : new TreeSet(usedLabels); - this.sequential = isSequential(usedLabels); - } + boolean hasBindings() { + return !bindings.isEmpty(); + } - private static boolean isSequential(Set usedLabels) { + boolean hasNamedBindings() { + return this.hasNamedBindings; + } - for (int i = 0; i < usedLabels.size(); i++) { + boolean containsPageableInSpel() { + return containsPageableInSpel; + } - if (usedLabels.contains(i + 1)) { - continue; + boolean usesJdbcStyleParameters() { + return usesJdbcStyleParameters; + } + + List getBindings() { + return Collections.unmodifiableList(bindings); + } + + /** + * Derive a query (typically a count query) from the given query string. We need to copy expression bindings from the + * declared to the derived query as JPQL query derivation only sees JPA parameter markers and not the original + * expressions anymore. + * + * @return + */ + @Override + public PreprocessedQuery rewrite(String newQueryString) { + + return ParameterBindingParser.INSTANCE.parse(newQueryString, source::rewrite, derivedBindings -> { + + // need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees + // JPA parameter markers and not the original expressions anymore. + if (this.hasBindings() && !this.bindings.equals(derivedBindings)) { + + for (ParameterBinding binding : bindings) { + + Predicate identifier = binding::bindsTo; + Predicate notCompatible = Predicate.not(binding::isCompatibleWith); + + // replace incompatible bindings + if (derivedBindings.removeIf(it -> identifier.test(it) && notCompatible.test(it))) { + derivedBindings.add(binding); + } } - - return false; } + }); + } - return true; - } - - /** - * Allocate the next index label (1-based). - * - * @return the next index label. - */ - public int allocate() { - - if (sequential) { - int index = usedLabels.size() + 1; - usedLabels.add(index); - - return index; - } - - int attempts = usedLabels.last() + 1; - int index = attemptAllocate(attempts); - - if (index == -1) { - throw new IllegalStateException( - "Unable to allocate a unique parameter label. All possible labels have been used."); - } - - usedLabels.add(index); - - return index; - } - - private int attemptAllocate(int attempts) { - - for (int i = 0; i < attempts; i++) { - - if (usedLabels.contains(i + 1)) { - continue; - } - - return i + 1; - } - - return -1; - } - - public boolean hasLabels() { - return !usedLabels.isEmpty(); - } + @Override + public String toString() { + return "ParametrizedQuery[" + source + ", " + bindings + ']'; } /** @@ -333,7 +186,7 @@ class StringQuery implements EntityQuery { private static final Pattern NAMED_STYLE_PARAM = Pattern.compile("(?!\\\\):\\w+"); // no \ and :[text] private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type; " - + "Already have: %s, found %s; If you bind a parameter multiple times make sure they use the same binding"; + + "Already have: %s, found %s; If you bind a parameter multiple times make sure they use the same binding"; private static final int INDEXED_PARAMETER_GROUP = 4; private static final int NAMED_PARAMETER_GROUP = 6; private static final int COMPARISION_TYPE_GROUP = 1; @@ -371,19 +224,24 @@ class StringQuery implements EntityQuery { * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns * the cleaned up query. */ - BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(DeclaredQuery query) { + PreprocessedQuery parse(String query, Function declaredQueryFactory, + Consumer> parameterBindingPostProcessor) { IndexedParameterLabels parameterLabels = new IndexedParameterLabels(findParameterIndices(query)); boolean parametersShouldBeAccessedByIndex = parameterLabels.hasLabels(); + List bindings = new ArrayList<>(); + boolean jdbcStyle = false; + boolean containsPageableInSpel = query.contains("#pageable"); + /* * Prefer indexed access over named parameters if only SpEL Expression parameters are present. */ - if (!parametersShouldBeAccessedByIndex && query.getQueryString().contains("?#{")) { + if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) { parametersShouldBeAccessedByIndex = true; } - ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query.getQueryString(), + ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query, parametersShouldBeAccessedByIndex, parameterLabels); String resultingQuery = parsedQuery.getQueryString(); @@ -409,7 +267,8 @@ class StringQuery implements EntityQuery { jdbcStyle = true; } - if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) { + if (NUMBERED_STYLE_PARAM.matcher(match) + .find() || NAMED_STYLE_PARAM.matcher(match).find()) { usesJpaStyleParameters = true; } @@ -429,56 +288,64 @@ class StringQuery implements EntityQuery { parameterIndex = parameterLabels.allocate(); } - BindingIdentifier queryParameter; + ParameterBinding.BindingIdentifier queryParameter; if (parameterIndex != null) { - queryParameter = BindingIdentifier.of(parameterIndex); - } else if (parameterName != null) { - queryParameter = BindingIdentifier.of(parameterName); - } else { + queryParameter = ParameterBinding.BindingIdentifier.of(parameterIndex); + } + else if (parameterName != null) { + queryParameter = ParameterBinding.BindingIdentifier.of(parameterName); + } + else { throw new IllegalStateException("No bindable expression found"); } - ParameterOrigin origin = ObjectUtils.isEmpty(expression) - ? ParameterOrigin.ofParameter(parameterName, parameterIndex) - : ParameterOrigin.ofExpression(expression); + ParameterBinding.ParameterOrigin origin = ObjectUtils.isEmpty(expression) + ? ParameterBinding.ParameterOrigin.ofParameter(parameterName, parameterIndex) + : ParameterBinding.ParameterOrigin.ofExpression(expression); - BindingIdentifier targetBinding = queryParameter; - Function bindingFactory = switch (ParameterBindingType.of(typeSource)) { + ParameterBinding.BindingIdentifier targetBinding = queryParameter; + Function bindingFactory = switch (ParameterBindingType + .of(typeSource)) { case LIKE -> { - Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); - yield (identifier) -> new LikeParameterBinding(identifier, origin, likeType); + Part.Type likeType = ParameterBinding.LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); + yield (identifier) -> new ParameterBinding.LikeParameterBinding(identifier, origin, likeType); } - case IN -> (identifier) -> new InParameterBinding(identifier, origin); // fall-through we don't need a special - // parameter queryParameter for the - // given parameter. + case IN -> + (identifier) -> new ParameterBinding.InParameterBinding(identifier, origin); // fall-through we + // don't need a special + // parameter queryParameter for the + // given parameter. default -> (identifier) -> new ParameterBinding(identifier, origin); }; if (origin.isExpression()) { parameterBindings.register(bindingFactory.apply(queryParameter)); - } else { + } + else { targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory, parameterLabels); } replacement = targetBinding.hasName() ? ":" + targetBinding.getName() - : ((!usesJpaStyleParameters && jdbcStyle) ? "?" - : "?" + targetBinding.getPosition()); + : ((!usesJpaStyleParameters && jdbcStyle) ? "?" : "?" + targetBinding.getPosition()); String result; String substring = matcher.group(2); int index = resultingQuery.indexOf(substring, currentIndex); if (index < 0) { result = resultingQuery; - } else { + } + else { currentIndex = index + replacement.length(); result = resultingQuery.substring(0, index) + replacement - + resultingQuery.substring(index + substring.length()); + + resultingQuery.substring(index + substring.length()); } resultingQuery = result; } - return new BindableQuery(query, resultingQuery, bindings, jdbcStyle); + parameterBindingPostProcessor.accept(bindings); + return new PreprocessedQuery(declaredQueryFactory.apply(resultingQuery), bindings, jdbcStyle, + containsPageableInSpel); } private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, @@ -586,18 +453,17 @@ class StringQuery implements EntityQuery { } } - - /** * Utility to create unique parameter bindings for LIKE that refer to the same underlying method parameter but are - * bound to potentially unique query parameters for {@link LikeParameterBinding#prepare(Object) LIKE rewrite}. + * bound to potentially unique query parameters for {@link ParameterBinding.LikeParameterBinding#prepare(Object) LIKE + * rewrite}. * * @author Mark Paluch * @since 3.1.2 */ - static class ParameterBindings { + private static class ParameterBindings { - private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); + private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); private final Consumer registration; @@ -611,21 +477,22 @@ class StringQuery implements EntityQuery { } /** - * Return whether the identifier is already bound. - * * @param identifier - * @return + * @return whether the identifier is already bound. */ - public boolean isBound(BindingIdentifier identifier) { + public boolean isBound(ParameterBinding.BindingIdentifier identifier) { return !getBindings(identifier).isEmpty(); } - BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, - Function bindingFactory, IndexedParameterLabels parameterLabels) { + ParameterBinding.BindingIdentifier register(ParameterBinding.BindingIdentifier identifier, + ParameterBinding.ParameterOrigin origin, + Function bindingFactory, + IndexedParameterLabels parameterLabels) { - Assert.isInstanceOf(MethodInvocationArgument.class, origin); + Assert.isInstanceOf(ParameterBinding.MethodInvocationArgument.class, origin); - BindingIdentifier methodArgument = ((MethodInvocationArgument) origin).identifier(); + ParameterBinding.BindingIdentifier methodArgument = ((ParameterBinding.MethodInvocationArgument) origin) + .identifier(); List bindingsForOrigin = getBindings(methodArgument); if (!isBound(identifier)) { @@ -645,7 +512,7 @@ class StringQuery implements EntityQuery { } } - BindingIdentifier syntheticIdentifier; + ParameterBinding.BindingIdentifier syntheticIdentifier; if (identifier.hasName() && methodArgument.hasName()) { int index = 0; @@ -654,9 +521,10 @@ class StringQuery implements EntityQuery { index++; newName = methodArgument.getName() + "_" + index; } - syntheticIdentifier = BindingIdentifier.of(newName); - } else { - syntheticIdentifier = BindingIdentifier.of(parameterLabels.allocate()); + syntheticIdentifier = ParameterBinding.BindingIdentifier.of(newName); + } + else { + syntheticIdentifier = ParameterBinding.BindingIdentifier.of(parameterLabels.allocate()); } ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier); @@ -666,11 +534,12 @@ class StringQuery implements EntityQuery { } private boolean existsBoundParameter(String key) { - return methodArgumentToLikeBindings.values().stream().flatMap(Collection::stream) + return methodArgumentToLikeBindings.values().stream() + .flatMap(Collection::stream) .anyMatch(it -> key.equals(it.getName())); } - private List getBindings(BindingIdentifier identifier) { + private List getBindings(ParameterBinding.BindingIdentifier identifier) { return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>()); } @@ -678,4 +547,79 @@ class StringQuery implements EntityQuery { registration.accept(parameterBinding); } } + + /** + * Value object to track and allocate used parameter index labels in a query. + */ + static class IndexedParameterLabels { + + private final TreeSet usedLabels; + private final boolean sequential; + + public IndexedParameterLabels(Set usedLabels) { + + this.usedLabels = usedLabels instanceof TreeSet ts ? ts : new TreeSet(usedLabels); + this.sequential = isSequential(usedLabels); + } + + private static boolean isSequential(Set usedLabels) { + + for (int i = 0; i < usedLabels.size(); i++) { + + if (usedLabels.contains(i + 1)) { + continue; + } + + return false; + } + + return true; + } + + /** + * Allocate the next index label (1-based). + * + * @return the next index label. + */ + public int allocate() { + + if (sequential) { + int index = usedLabels.size() + 1; + usedLabels.add(index); + + return index; + } + + int attempts = usedLabels.last() + 1; + int index = attemptAllocate(attempts); + + if (index == -1) { + throw new IllegalStateException( + "Unable to allocate a unique parameter label. All possible labels have been used."); + } + + usedLabels.add(index); + + return index; + } + + private int attemptAllocate(int attempts) { + + for (int i = 0; i < attempts; i++) { + + if (usedLabels.contains(i + 1)) { + continue; + } + + return i + 1; + } + + return -1; + } + + public boolean hasLabels() { + return !usedLabels.isEmpty(); + } + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java index 528426f82..2810f957c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java @@ -15,11 +15,9 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; - -import org.jspecify.annotations.Nullable; import org.springframework.data.repository.query.ReturnedType; /** @@ -27,10 +25,23 @@ import org.springframework.data.repository.query.ReturnedType; * * @author Diego Krupitza * @author Greg Turnquist - * @since 2.7.0 + * @author Mark Paluch + * @since 2.7 */ public interface QueryEnhancer { + /** + * Creates a new {@link QueryEnhancer} for a {@link DeclaredQuery}. Convenience method for + * {@link QueryEnhancerFactory#create(QueryProvider)}. + * + * @param query the query to be enhanced. + * @return the new {@link QueryEnhancer}. + * @since 4.0 + */ + static QueryEnhancer create(DeclaredQuery query) { + return QueryEnhancerFactory.forQuery(query).create(query); + } + /** * Returns whether the given JPQL query contains a constructor expression. * @@ -39,9 +50,9 @@ public interface QueryEnhancer { boolean hasConstructorExpression(); /** - * Resolves the alias for the entity to be retrieved from the given JPA query. + * Resolves the primary alias for the entity to be retrieved from the given JPA query. * - * @return Might return {@literal null}. + * @return can return {@literal null}. */ @Nullable String detectAlias(); @@ -53,60 +64,24 @@ public interface QueryEnhancer { */ String getProjection(); - /** - * Returns the join aliases of the query. - * - * @return the join aliases of the query. - */ - @Deprecated(forRemoval = true) - Set getJoinAliases(); - /** * Gets the query we want to use for enhancements. * * @return non-null {@link DeclaredQuery} that wraps the query. */ - StructuredQuery getQuery(); - - /** - * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to. - * - * @param sort the sort specification to apply. - * @return the modified query string. - */ - String applySorting(Sort sort); - - /** - * Adds {@literal order by} clause to the JPQL query. - * - * @param sort the sort specification to apply. - * @param alias the alias to be used in the order by clause. May be {@literal null} or empty. - * @return the modified query string. - * @deprecated since 3.5, use {@link #rewrite(QueryRewriteInformation)} instead. - */ - @Deprecated(since = "3.5", forRemoval = true) - String applySorting(Sort sort, @Nullable String alias); + QueryProvider getQuery(); /** * Rewrite the query to include sorting and apply {@link ReturnedType} customizations. * * @param rewriteInformation the rewrite information to apply. * @return the modified query string. - * @since 3.5 + * @since 4.0 */ String rewrite(QueryRewriteInformation rewriteInformation); /** - * Creates a count projected query from the given original query. - * - * @return Guaranteed to be not {@literal null}. - */ - default String createCountQueryFor() { - return createCountQueryFor(null); - } - - /** - * Creates a count projected query from the given original query using the provided countProjection. + * Creates a count projected query from the given original query using the provided {@code countProjection}. * * @param countProjection may be {@literal null}. * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}. @@ -116,7 +91,7 @@ public interface QueryEnhancer { /** * Interface to describe the information needed to rewrite a query. * - * @since 3.5 + * @since 4.0 */ interface QueryRewriteInformation { @@ -129,6 +104,7 @@ public interface QueryEnhancer { * @return type expected to be returned by the query. */ ReturnedType getReturnedType(); + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java index 07ed8642c..face0778a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java @@ -25,10 +25,11 @@ import org.springframework.util.ClassUtils; * Pre-defined QueryEnhancerFactories to be used for query enhancement. * * @author Mark Paluch + * @since 4.0 */ public class QueryEnhancerFactories { - private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class); + private static final Log LOG = LogFactory.getLog(QueryEnhancerFactories.class); static final boolean jSqlParserPresent = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser", QueryEnhancerFactory.class.getClassLoader()); @@ -57,7 +58,7 @@ public class QueryEnhancerFactories { } @Override - public QueryEnhancer create(StructuredQuery query) { + public QueryEnhancer create(QueryProvider query) { return new DefaultQueryEnhancer(query); } }, @@ -65,11 +66,12 @@ public class QueryEnhancerFactories { JSQLPARSER { @Override public boolean supports(DeclaredQuery query) { - return query.isNativeQuery(); + return query.isNative(); } @Override - public QueryEnhancer create(StructuredQuery query) { + public QueryEnhancer create(QueryProvider query) { + if (jSqlParserPresent) { return new JSqlParserQueryEnhancer(query); } @@ -81,33 +83,33 @@ public class QueryEnhancerFactories { HQL { @Override public boolean supports(DeclaredQuery query) { - return !query.isNativeQuery(); + return query.isJpql(); } @Override - public QueryEnhancer create(StructuredQuery query) { + public QueryEnhancer create(QueryProvider query) { return JpaQueryEnhancer.forHql(query.getQueryString()); } }, EQL { @Override public boolean supports(DeclaredQuery query) { - return !query.isNativeQuery(); + return query.isJpql(); } @Override - public QueryEnhancer create(StructuredQuery query) { + public QueryEnhancer create(QueryProvider query) { return JpaQueryEnhancer.forEql(query.getQueryString()); } }, JPQL { @Override public boolean supports(DeclaredQuery query) { - return !query.isNativeQuery(); + return query.isJpql(); } @Override - public QueryEnhancer create(StructuredQuery query) { + public QueryEnhancer create(QueryProvider query) { return JpaQueryEnhancer.forJpql(query.getQueryString()); } } @@ -165,4 +167,5 @@ public class QueryEnhancerFactories { public static QueryEnhancerFactory jpql() { return BuiltinQueryEnhancerFactories.JPQL; } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java index 26bdf4b5b..023379859 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java @@ -16,13 +16,13 @@ package org.springframework.data.jpa.repository.query; /** - * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link IntrospectedQuery}. + * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link ParametrizedQuery}. * * @author Diego Krupitza * @author Greg Turnquist * @author Mark Paluch * @author Christoph Strobl - * @since 2.7 + * @since 4.0 */ public interface QueryEnhancerFactory { @@ -38,9 +38,9 @@ public interface QueryEnhancerFactory { * Creates a new {@link QueryEnhancer} for the given query. * * @param query the query to be enhanced and introspected. - * @return + * @return the query enhancer to be used. */ - QueryEnhancer create(StructuredQuery query); + QueryEnhancer create(QueryProvider query); /** * Creates a new {@link QueryEnhancerFactory} for the given {@link DeclaredQuery}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java index 93268c638..fd5f1da6a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java @@ -21,9 +21,10 @@ import org.springframework.data.jpa.provider.PersistenceProvider; * Interface declaring a strategy to select a {@link QueryEnhancer} for a given {@link DeclaredQuery query}. *

* Enhancers are selected when introspecting a query to determine their selection, joins, aliases and other information - * so that query methods can derive count queries, apply sorting and perform other transformations. + * so that query methods can derive count queries, apply sorting and perform other rewrite transformations. * * @author Mark Paluch + * @since 4.0 */ public interface QueryEnhancerSelector { @@ -90,4 +91,5 @@ public interface QueryEnhancerSelector { } } + } 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 3a9d2af87..6d6196b8e 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 @@ -20,9 +20,9 @@ import jakarta.persistence.TemporalType; import java.util.function.Function; -import org.springframework.data.expression.ValueEvaluationContext; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueExpression; import org.springframework.data.expression.ValueExpressionParser; @@ -54,7 +54,7 @@ abstract class QueryParameterSetterFactory { * @param binding the parameter binding to create a {@link QueryParameterSetter} for. * @return */ - abstract @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery); + abstract @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery parametrizedQuery); /** * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. @@ -180,7 +180,7 @@ abstract class QueryParameterSetterFactory { } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery parametrizedQuery) { if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) { return null; @@ -212,7 +212,7 @@ abstract class QueryParameterSetterFactory { private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory { @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) { if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) { return null; @@ -248,7 +248,7 @@ abstract class QueryParameterSetterFactory { } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) { Assert.notNull(binding, "Binding must not be null"); @@ -294,7 +294,7 @@ abstract class QueryParameterSetterFactory { } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) { if (binding instanceof ParameterBinding.PartTreeParameterBinding ptb) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java similarity index 63% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java index 6ba9f81ba..98de7da6e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java @@ -16,23 +16,22 @@ package org.springframework.data.jpa.repository.query; /** + * Interface indicating an object that contains and exposes an {@code query string}. This can be either a JPQL query + * string or a SQL query string. + * * @author Christoph Strobl + * @author Mark Paluch + * @since 4.0 + * @see DeclaredQuery#jpqlQuery(String) + * @see DeclaredQuery#nativeQuery(String) */ -final class NativeQuery implements DeclaredQuery { +public interface QueryProvider { - private final String sql; + /** + * Return the query string. + * + * @return the query string. + */ + String getQueryString(); - NativeQuery(String sql) { - this.sql = sql; - } - - @Override - public boolean isNativeQuery() { - return true; - } - - @Override - public String getQueryString() { - return sql; - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index 1619dedb8..b16d2ef5d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -445,10 +445,8 @@ public abstract class QueryUtils { * * @param query must not be {@literal null}. * @return Might return {@literal null}. - * @deprecated use {@link IntrospectedQuery#getAlias()} instead. */ - @Deprecated - public static @Nullable String detectAlias(String query) { + static @Nullable String detectAlias(String query) { String alias = null; Matcher matcher = ALIAS_MATCH.matcher(removeSubqueries(query)); @@ -554,10 +552,8 @@ public abstract class QueryUtils { * * @param originalQuery must not be {@literal null} or empty. * @return Guaranteed to be not {@literal null}. - * @deprecated use {@link IntrospectedQuery#deriveCountQuery(String)} instead. */ - @Deprecated - public static String createCountQueryFor(String originalQuery) { + static String createCountQueryFor(String originalQuery) { return createCountQueryFor(originalQuery, null); } @@ -568,10 +564,8 @@ public abstract class QueryUtils { * @param countProjection may be {@literal null}. * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}. * @since 1.6 - * @deprecated use {@link IntrospectedQuery#deriveCountQuery(String)} instead. */ - @Deprecated - public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) { + static String createCountQueryFor(String originalQuery, @Nullable String countProjection) { return createCountQueryFor(originalQuery, countProjection, false); } 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 b913061ad..b042318b1 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 @@ -18,11 +18,9 @@ package org.springframework.data.jpa.repository.query; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; -import org.springframework.data.jpa.repository.QueryRewriter; - import org.jspecify.annotations.Nullable; + import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.data.repository.query.ValueExpressionDelegate; /** * {@link RepositoryQuery} implementation that inspects a {@link org.springframework.data.repository.query.QueryMethod} @@ -41,14 +39,14 @@ class SimpleJpaQuery extends AbstractStringBasedJpaQuery { * * @param method must not be {@literal null}. * @param em must not be {@literal null}. - * @param queryString must not be {@literal null} or empty. - * @param countQueryString can be {@literal null} if not defined. + * @param query must not be {@literal null} or empty. + * @param countQuery can be {@literal null} if not defined. * @param queryConfiguration must not be {@literal null}. */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, - JpaQueryConfiguration queryConfiguration) { + public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query, + @Nullable DeclaredQuery countQuery, JpaQueryConfiguration queryConfiguration) { - super(method, em, queryString, countQueryString, queryConfiguration); + super(method, em, query, countQuery, queryConfiguration); 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/StructuredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java deleted file mode 100644 index 2ebfcb054..000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.jpa.repository.query; - -/** - * @author Christoph Strobl - */ -public interface StructuredQuery { - - String getQueryString(); -} 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/TemplatedQuery.java similarity index 63% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/TemplatedQuery.java index b6c93b560..487a7b11f 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/TemplatedQuery.java @@ -23,12 +23,11 @@ import org.springframework.core.env.StandardEnvironment; 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.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.util.Assert; /** - * Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression. + * Factory methods to obtain {@link EntityQuery} from a declared query using SpEL template-expressions. *

* Currently, the following template variables are available: *

    @@ -42,7 +41,7 @@ import org.springframework.util.Assert; * @author Diego Krupitza * @author Greg Turnquist */ -class ExpressionBasedStringQuery extends StringQuery { +class TemplatedQuery { private static final String EXPRESSION_PARAMETER = "$1#{"; private static final String QUOTED_EXPRESSION_PARAMETER = "$1__HASH__{"; @@ -61,18 +60,35 @@ class ExpressionBasedStringQuery extends StringQuery { } /** - * Creates a new {@link ExpressionBasedStringQuery} for the given query and {@link EntityMetadata}. + * Create a {@link DefaultEntityQuery} given {@link String query}, {@link JpaQueryMethod} and + * {@link JpaQueryConfiguration}. * - * @param query must not be {@literal null} or empty. - * @param metadata must not be {@literal null}. - * @param parser must not be {@literal null}. - * @param nativeQuery is a given query is native or not. - * @param selector must not be {@literal null}. + * @param queryString must not be {@literal null}. + * @param queryMethod must not be {@literal null}. + * @param queryContext must not be {@literal null}. + * @return the created {@link DefaultEntityQuery}. */ - ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, ValueExpressionParser parser, - boolean nativeQuery, QueryEnhancerSelector selector) { - super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query), - selector, parameterBindings -> {}); + public static EntityQuery create(String queryString, JpaQueryMethod queryMethod, JpaQueryConfiguration queryContext) { + return create(queryMethod.getDeclaredQuery(queryString), queryMethod.getEntityInformation(), queryContext); + } + + /** + * Create a {@link DefaultEntityQuery} given {@link DeclaredQuery query}, {@link JpaEntityMetadata} and + * {@link JpaQueryConfiguration}. + * + * @param declaredQuery must not be {@literal null}. + * @param entityMetadata must not be {@literal null}. + * @param queryContext must not be {@literal null}. + * @return the created {@link DefaultEntityQuery}. + */ + public static EntityQuery create(DeclaredQuery declaredQuery, JpaEntityMetadata entityMetadata, + JpaQueryConfiguration queryContext) { + + ValueExpressionParser expressionParser = queryContext.getValueExpressionDelegate().getValueExpressionParser(); + String resolvedExpressionQuery = renderQueryIfExpressionOrReturnQuery(declaredQuery.getQueryString(), + entityMetadata, expressionParser); + + return EntityQuery.create(declaredQuery.rewrite(resolvedExpressionQuery), queryContext.getSelector()); } /** @@ -80,7 +96,7 @@ class ExpressionBasedStringQuery extends StringQuery { * @param metadata the {@link JpaEntityMetadata} for the given entity. Must not be {@literal null}. * @param parser Must not be {@literal null}. */ - private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEntityMetadata metadata, + static String renderQueryIfExpressionOrReturnQuery(String query, JpaEntityMetadata metadata, ValueExpressionParser parser) { Assert.notNull(query, "query must not be null"); @@ -91,15 +107,14 @@ class ExpressionBasedStringQuery extends StringQuery { return query; } - StandardEvaluationContext evalContext = new StandardEvaluationContext(); + SimpleEvaluationContext evalContext = SimpleEvaluationContext.forReadOnlyDataBinding().build(); evalContext.setVariable(ENTITY_NAME, metadata.getEntityName()); query = potentiallyQuoteExpressionsParameter(query); ValueExpression expr = parser.parse(query); - String result = Objects.toString( - expr.evaluate(ValueEvaluationContext.of(DEFAULT_ENVIRONMENT, evalContext))); + String result = Objects.toString(expr.evaluate(ValueEvaluationContext.of(DEFAULT_ENVIRONMENT, evalContext))); if (result == null) { return query; @@ -120,10 +135,4 @@ class ExpressionBasedStringQuery extends StringQuery { return query.contains(ENTITY_NAME_VARIABLE_EXPRESSION); } - public static StringQuery create(String query, JpaQueryMethod method, JpaQueryConfiguration queryContext) { - return new ExpressionBasedStringQuery(query, method.getEntityInformation(), - queryContext.getValueExpressionDelegate().getValueExpressionParser(), - method.isNativeQuery(), queryContext.getSelector()); - } - } 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 204471b6d..3d77980fb 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 @@ -68,9 +68,10 @@ class AbstractStringBasedJpaQueryIntegrationTests { when(mock.getMetamodel()).thenReturn(em.getMetamodel()); JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class); - AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, method.getAnnotatedQuery(), null, CONFIG); + AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, method.getRequiredDeclaredQuery(), null, + CONFIG); - jpaQuery.createJpaQuery(method.getAnnotatedQuery(), Sort.unsorted(), null, + jpaQuery.createJpaQuery(method.getRequiredDeclaredQuery(), Sort.unsorted(), null, method.getResultProcessor().getReturnedType()); verify(mock, times(1)).createQuery(anyString()); 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 adc489cc9..953203134 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 @@ -150,7 +150,7 @@ class AbstractStringBasedJpaQueryUnitTests { } @Override - protected String applySorting(CachableQuery query) { + protected QueryProvider applySorting(CachableQuery query) { captureInvocation("applySorting", query); @@ -158,12 +158,13 @@ class AbstractStringBasedJpaQueryUnitTests { } @Override - protected jakarta.persistence.Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable, + protected jakarta.persistence.Query createJpaQuery(QueryProvider query, Sort sort, + @Nullable Pageable pageable, ReturnedType returnedType) { - captureInvocation("createJpaQuery", queryString, sort, pageable, returnedType); + captureInvocation("createJpaQuery", query, sort, pageable, returnedType); - jakarta.persistence.Query jpaQuery = super.createJpaQuery(queryString, sort, pageable, returnedType); + jakarta.persistence.Query jpaQuery = super.createJpaQuery(query, sort, pageable, returnedType); return jpaQuery == null ? Mockito.mock(jakarta.persistence.Query.class) : jpaQuery; } 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/DefaultEntityQueryUnitTests.java similarity index 85% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java index 155b291a5..3077ded6b 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/DefaultEntityQueryUnitTests.java @@ -28,11 +28,10 @@ import org.springframework.data.jpa.repository.query.ParameterBinding.InParamete 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.jpa.repository.query.StringQuery.ParameterBindingParser; import org.springframework.data.repository.query.parser.Part.Type; /** - * Unit tests for {@link StringQuery}. + * Unit tests for {@link DefaultEntityQuery}. * * @author Oliver Gierke * @author Thomas Darimont @@ -44,13 +43,13 @@ import org.springframework.data.repository.query.parser.Part.Type; * @author Aleksei Elin * @author Gunha Hwang */ -class StringQueryUnitTests { +class DefaultEntityQueryUnitTests { @Test // DATAJPA-341 void doesNotConsiderPlainLikeABinding() { String source = "select u from User u where u.firstname like :firstname"; - StringQuery query = new StringQuery(source, false); + DefaultEntityQuery query = new TestEntityQuery(source, false); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo(source); @@ -67,8 +66,8 @@ class StringQueryUnitTests { @Test // DATAJPA-292 void detectsPositionalLikeBindings() { - StringQuery query = new StringQuery("select u from User u where u.firstname like %?1% or u.lastname like %?2", - true); + DefaultEntityQuery query = new TestEntityQuery( + "select u from User u where u.firstname like %?1% or u.lastname like %?2", true); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()) @@ -91,7 +90,7 @@ class StringQueryUnitTests { @Test // DATAJPA-292, GH-3041 void detectsAnonymousLikeBindings() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where u.firstname like %?% or u.lastname like %? or u.lastname=?", true); assertThat(query.hasParameterBindings()).isTrue(); @@ -117,7 +116,8 @@ class StringQueryUnitTests { @Test // DATAJPA-292, GH-3041 void detectsNamedLikeBindings() { - StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname", true); + DefaultEntityQuery query = new TestEntityQuery("select u from User u where u.firstname like %:firstname", + true); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname"); @@ -134,7 +134,7 @@ class StringQueryUnitTests { @Test // GH-3041 void rewritesNamedLikeToUniqueParametersIfNecessary() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where u.firstname like %:firstname or u.firstname like :firstname% or u.firstname = :firstname", true); @@ -165,7 +165,7 @@ class StringQueryUnitTests { @Test // GH-3784 void rewritesNamedLikeToUniqueParametersRetainingCountQuery() { - DeclaredQuery query = new StringQuery( + ParametrizedQuery query = new TestEntityQuery( "select u from User u where u.firstname like %:firstname or u.firstname like :firstname% or u.firstname = :firstname", false).deriveCountQuery(null); @@ -198,7 +198,7 @@ class StringQueryUnitTests { @Test // GH-3784 void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() { - DeclaredQuery query = new StringQuery( + ParametrizedQuery query = new TestEntityQuery( "select u from User u where u.firstname like %:#{firstname} or u.firstname like :#{firstname}%", false) .deriveCountQuery(null); @@ -225,7 +225,7 @@ class StringQueryUnitTests { @Test // GH-3041 void rewritesPositionalLikeToUniqueParametersIfNecessary() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where u.firstname like %?1 or u.firstname like ?1% or u.firstname = ?1", true); assertThat(query.hasParameterBindings()).isTrue(); @@ -239,7 +239,7 @@ class StringQueryUnitTests { @Test // GH-3041 void reusesNamedLikeBindingsWherePossible() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where u.firstname like %:firstname or u.firstname like %:firstname% or u.firstname like %:firstname% or u.firstname like %:firstname", true); @@ -247,7 +247,8 @@ class StringQueryUnitTests { assertThat(query.getQueryString()).isEqualTo( "select u from User u where u.firstname like :firstname or u.firstname like :firstname_1 or u.firstname like :firstname_1 or u.firstname like :firstname"); - query = new StringQuery("select u from User u where u.firstname like %:firstname or u.firstname =:firstname", true); + query = new TestEntityQuery( + "select u from User u where u.firstname like %:firstname or u.firstname =:firstname", true); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()) @@ -257,7 +258,7 @@ class StringQueryUnitTests { @Test // GH-3041 void reusesPositionalLikeBindingsWherePossible() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where u.firstname like %?1 or u.firstname like %?1% or u.firstname like %?1% or u.firstname like %?1", false); @@ -265,7 +266,7 @@ class StringQueryUnitTests { assertThat(query.getQueryString()).isEqualTo( "select u from User u where u.firstname like ?1 or u.firstname like ?2 or u.firstname like ?2 or u.firstname like ?1"); - query = new StringQuery("select u from User u where u.firstname like %?1 or u.firstname =?1", false); + query = new TestEntityQuery("select u from User u where u.firstname like %?1 or u.firstname =?1", false); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like ?1 or u.firstname =?2"); @@ -274,7 +275,7 @@ class StringQueryUnitTests { @Test // GH-3041 void shouldRewritePositionalBindingsWithParameterReuse() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where u.firstname like ?2 or u.firstname like %?2% or u.firstname like %?1% or u.firstname like %?1 OR u.firstname like ?1", false); @@ -296,8 +297,8 @@ class StringQueryUnitTests { @Test // GH-3758 void createsDistinctBindingsForIndexedSpel() { - StringQuery query = new StringQuery("select u from User u where u.firstname = ?#{foo} OR u.firstname = ?#{foo}", - false); + DefaultEntityQuery query = new TestEntityQuery( + "select u from User u where u.firstname = ?#{foo} OR u.firstname = ?#{foo}", false); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getParameterBindings()).hasSize(2).extracting(ParameterBinding::getRequiredPosition) @@ -310,8 +311,8 @@ class StringQueryUnitTests { @Test // GH-3758 void createsDistinctBindingsForNamedSpel() { - StringQuery query = new StringQuery("select u from User u where u.firstname = :#{foo} OR u.firstname = :#{foo}", - false); + DefaultEntityQuery query = new TestEntityQuery( + "select u from User u where u.firstname = :#{foo} OR u.firstname = :#{foo}", false); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getParameterBindings()).hasSize(2).extracting(ParameterBinding::getOrigin) @@ -323,7 +324,7 @@ class StringQueryUnitTests { void detectsNamedInParameterBindings() { String queryString = "select u from User u where u.id in :ids"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo(queryString); @@ -338,7 +339,7 @@ class StringQueryUnitTests { void detectsMultipleNamedInParameterBindings() { String queryString = "select u from User u where u.id in :ids and u.name in :names and foo = :bar"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo(queryString); @@ -355,7 +356,7 @@ class StringQueryUnitTests { void deriveCountQueryWithNamedInRetainsOrigin() { String queryString = "select u from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins)"; - DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null); + ParametrizedQuery query = new TestEntityQuery(queryString, false).deriveCountQuery(null); assertThat(query.getQueryString()) .isEqualTo("select count(u) from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins_1)"); @@ -376,7 +377,7 @@ class StringQueryUnitTests { void deriveCountQueryWithPositionalInRetainsOrigin() { String queryString = "select u from User u where (?1) IS NULL OR LOWER(u.login) IN (?1)"; - DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null); + ParametrizedQuery query = new TestEntityQuery(queryString, false).deriveCountQuery(null); assertThat(query.getQueryString()) .isEqualTo("select count(u) from User u where (?1) IS NULL OR LOWER(u.login) IN (?2)"); @@ -397,7 +398,7 @@ class StringQueryUnitTests { void detectsPositionalInParameterBindings() { String queryString = "select u from User u where u.id in ?1"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo(queryString); @@ -411,7 +412,7 @@ class StringQueryUnitTests { @Test // GH-3126 void allowsReuseOfParameterWithInAndRegularBinding() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where COALESCE(?1) is null OR u.id in ?1 OR COALESCE(?1) is null OR u.id in ?1", true); assertThat(query.hasParameterBindings()).isTrue(); @@ -424,7 +425,7 @@ class StringQueryUnitTests { assertPositionalBinding(ParameterBinding.class, 1, bindings.get(0)); assertPositionalBinding(InParameterBinding.class, 2, bindings.get(1)); - query = new StringQuery( + query = new TestEntityQuery( "select u from User u where COALESCE(:foo) is null OR u.id in :foo OR COALESCE(:foo) is null OR u.id in :foo", true); @@ -443,7 +444,7 @@ class StringQueryUnitTests { void detectsPositionalInParameterBindingsAndExpressions() { String queryString = "select u from User u where foo = ?#{bar} and bar = ?3 and baz = ?#{baz}"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.getQueryString()).isEqualTo("select u from User u where foo = ?1 and bar = ?3 and baz = ?2"); } @@ -452,7 +453,7 @@ class StringQueryUnitTests { void detectsPositionalInParameterBindingsAndExpressionsWithReuse() { String queryString = "select u from User u where foo = ?#{bar} and bar = ?2 and baz = ?#{bar}"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.getQueryString()).isEqualTo("select u from User u where foo = ?1 and bar = ?2 and baz = ?3"); } @@ -460,17 +461,17 @@ class StringQueryUnitTests { @Test // GH-3126 void countQueryDerivationRetainsNamedExpressionParameters() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where foo = :#{bar} ORDER BY CASE WHEN (u.firstname >= :#{name}) THEN 0 ELSE 1 END", false); - DeclaredQuery countQuery = query.deriveCountQuery(null); + ParametrizedQuery countQuery = query.deriveCountQuery(null); assertThat(countQuery.getParameterBindings()).hasSize(1); assertThat(countQuery.getParameterBindings()).extracting(ParameterBinding::getOrigin) .extracting(ParameterOrigin::isExpression).isEqualTo(List.of(true)); - query = new StringQuery( + query = new TestEntityQuery( "select u from User u where foo = :#{bar} and bar = :bar ORDER BY CASE WHEN (u.firstname >= :bar) THEN 0 ELSE 1 END", false); @@ -485,17 +486,17 @@ class StringQueryUnitTests { @Test // GH-3126 void countQueryDerivationRetainsIndexedExpressionParameters() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select u from User u where foo = ?#{bar} ORDER BY CASE WHEN (u.firstname >= ?#{name}) THEN 0 ELSE 1 END", false); - DeclaredQuery countQuery = query.deriveCountQuery(null); + ParametrizedQuery countQuery = query.deriveCountQuery(null); assertThat(countQuery.getParameterBindings()).hasSize(1); assertThat(countQuery.getParameterBindings()).extracting(ParameterBinding::getOrigin) .extracting(ParameterOrigin::isExpression).isEqualTo(List.of(true)); - query = new StringQuery( + query = new TestEntityQuery( "select u from User u where foo = ?#{bar} and bar = ?1 ORDER BY CASE WHEN (u.firstname >= ?1) THEN 0 ELSE 1 END", false); @@ -511,7 +512,7 @@ class StringQueryUnitTests { void detectsMultiplePositionalInParameterBindings() { String queryString = "select u from User u where u.id in ?1 and u.names in ?2 and foo = ?3"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo(queryString); @@ -527,13 +528,13 @@ class StringQueryUnitTests { @Test // DATAJPA-373 void handlesMultipleNamedLikeBindingsCorrectly() { - new StringQuery("select u from User u where u.firstname like %:firstname or foo like :bar", true); + new TestEntityQuery("select u from User u where u.firstname like %:firstname or foo like :bar", true); } @Test // DATAJPA-461 void treatsGreaterThanBindingAsSimpleBinding() { - StringQuery query = new StringQuery("select u from User u where u.createdDate > ?1", true); + DefaultEntityQuery query = new TestEntityQuery("select u from User u where u.createdDate > ?1", true); List bindings = query.getParameterBindings(); assertThat(bindings).hasSize(1); @@ -544,8 +545,10 @@ class StringQueryUnitTests { @Test // DATAJPA-473 void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() { - StringQuery query = new StringQuery("SELECT a FROM Article a WHERE a.overview LIKE %:escapedWord% ESCAPE '~'" - + " OR a.content LIKE %:escapedWord% ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC", true); + DefaultEntityQuery query = new TestEntityQuery( + "SELECT a FROM Article a WHERE a.overview LIKE %:escapedWord% ESCAPE '~'" + + " OR a.content LIKE %:escapedWord% ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC", + true); List bindings = query.getParameterBindings(); @@ -560,7 +563,8 @@ class StringQueryUnitTests { @Test // DATAJPA-483 void detectsInBindingWithParentheses() { - StringQuery query = new StringQuery("select count(we) from MyEntity we where we.status in (:statuses)", true); + DefaultEntityQuery query = new TestEntityQuery( + "select count(we) from MyEntity we where we.status in (:statuses)", true); List bindings = query.getParameterBindings(); @@ -571,7 +575,7 @@ class StringQueryUnitTests { @Test // DATAJPA-545 void detectsInBindingWithSpecialFrenchCharactersInParentheses() { - StringQuery query = new StringQuery("select * from MyEntity where abonnés in (:abonnés)", true); + DefaultEntityQuery query = new TestEntityQuery("select * from MyEntity where abonnés in (:abonnés)", true); List bindings = query.getParameterBindings(); @@ -582,7 +586,7 @@ class StringQueryUnitTests { @Test // DATAJPA-545 void detectsInBindingWithSpecialCharactersInParentheses() { - StringQuery query = new StringQuery("select * from MyEntity where øre in (:øre)", true); + DefaultEntityQuery query = new TestEntityQuery("select * from MyEntity where øre in (:øre)", true); List bindings = query.getParameterBindings(); @@ -593,7 +597,7 @@ class StringQueryUnitTests { @Test // DATAJPA-545 void detectsInBindingWithSpecialAsianCharactersInParentheses() { - StringQuery query = new StringQuery("select * from MyEntity where 생일 in (:생일)", true); + DefaultEntityQuery query = new TestEntityQuery("select * from MyEntity where 생일 in (:생일)", true); List bindings = query.getParameterBindings(); @@ -604,7 +608,7 @@ class StringQueryUnitTests { @Test // DATAJPA-545 void detectsInBindingWithSpecialCharactersAndWordCharactersMixedInParentheses() { - StringQuery query = new StringQuery("select * from MyEntity where foo in (:ab1babc생일233)", true); + DefaultEntityQuery query = new TestEntityQuery("select * from MyEntity where foo in (:ab1babc생일233)", true); List bindings = query.getParameterBindings(); @@ -615,7 +619,7 @@ class StringQueryUnitTests { @Test // DATAJPA-712, GH-3619 void shouldReplaceAllNamedExpressionParametersWithInClause() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "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(); @@ -626,7 +630,7 @@ class StringQueryUnitTests { @Test // DATAJPA-712 void shouldReplaceExpressionWithLikeParameters() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "select a from A a where a.b LIKE :#{#filter.login}% and a.c LIKE %:#{#filter.login}", true); String queryString = query.getQueryString(); @@ -637,8 +641,8 @@ class StringQueryUnitTests { @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} and a.d in ?${foo}", - true); + DefaultEntityQuery query = new TestEntityQuery( + "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 and a.d in ?3"); @@ -654,12 +658,11 @@ class StringQueryUnitTests { @Test // DATAJPA-864 void detectsConstructorExpressions() { - assertThat( - new StringQuery("select new com.example.Dto(a.foo, a.bar) from A a", false).hasConstructorExpression()) - .isTrue(); - assertThat(new StringQuery("select new com.example.Dto (a.foo, a.bar) from A a", false).hasConstructorExpression()) - .isTrue(); - assertThat(new StringQuery("select a from A a", true).hasConstructorExpression()).isFalse(); + assertThat(new TestEntityQuery("select new com.example.Dto(a.foo, a.bar) from A a", false) + .hasConstructorExpression()).isTrue(); + assertThat(new TestEntityQuery("select new com.example.Dto (a.foo, a.bar) from A a", false) + .hasConstructorExpression()).isTrue(); + assertThat(new TestEntityQuery("select a from A a", true).hasConstructorExpression()).isFalse(); } /** @@ -670,14 +673,16 @@ class StringQueryUnitTests { void detectsConstructorExpressionForDefaultConstructor() { // Parentheses required - assertThat(new StringQuery("select new com.example.Dto(a.name) from A a", false).hasConstructorExpression()) + assertThat( + new TestEntityQuery("select new com.example.Dto(a.name) from A a", false).hasConstructorExpression()) .isTrue(); } @Test // DATAJPA-1179 void bindingsMatchQueryForIdenticalSpelExpressions() { - StringQuery query = new StringQuery("select a from A a where a.first = :#{#exp} or a.second = :#{#exp}", true); + DefaultEntityQuery query = new TestEntityQuery( + "select a from A a where a.first = :#{#exp} or a.second = :#{#exp}", true); List bindings = query.getParameterBindings(); assertThat(bindings).isNotEmpty(); @@ -704,7 +709,7 @@ class StringQueryUnitTests { void checkProjection(String query, String expected, String description, boolean nativeQuery) { - assertThat(new StringQuery(query, nativeQuery).getProjection()) // + assertThat(new TestEntityQuery(query, nativeQuery).getProjection()) // .as("%s (%s)", description, query) // .isEqualTo(expected); } @@ -728,7 +733,7 @@ class StringQueryUnitTests { private void checkAlias(String query, String expected, String description, boolean nativeQuery) { - assertThat(new StringQuery(query, nativeQuery).getAlias()) // + assertThat(new TestEntityQuery(query, nativeQuery).getAlias()) // .as("%s (%s)", description, query) // .isEqualTo(expected); } @@ -781,7 +786,7 @@ class StringQueryUnitTests { void detectsMultiplePositionalParameterBindingsWithoutIndex() { String queryString = "select u from User u where u.id in ? and u.names in ? and foo = ?"; - StringQuery query = new StringQuery(queryString, false); + DefaultEntityQuery query = new TestEntityQuery(queryString, false); assertThat(query.getQueryString()).isEqualTo(queryString); assertThat(query.hasParameterBindings()).isTrue(); @@ -801,16 +806,18 @@ class StringQueryUnitTests { for (String testQuery : testQueries) { Assertions.assertThatExceptionOfType(IllegalArgumentException.class) // - .describedAs(testQuery).isThrownBy(() -> new StringQuery(testQuery, false)); + .describedAs(testQuery).isThrownBy(() -> new TestEntityQuery(testQuery, false)); } } @Test // DATAJPA-1307 void makesUsageOfJdbcStyleParameterAvailable() { - assertThat(new StringQuery("from Something something where something = ?", false).usesJdbcStyleParameters()) + assertThat( + new TestEntityQuery("from Something something where something = ?", false).usesJdbcStyleParameters()) .isTrue(); - assertThat(new StringQuery("from Something something where something =?", false).usesJdbcStyleParameters()) + assertThat( + new TestEntityQuery("from Something something where something =?", false).usesJdbcStyleParameters()) .isTrue(); List testQueries = Arrays.asList( // @@ -821,7 +828,7 @@ class StringQueryUnitTests { for (String testQuery : testQueries) { - assertThat(new StringQuery(testQuery, false) // + assertThat(new TestEntityQuery(testQuery, false) // .usesJdbcStyleParameters()) // .describedAs(testQuery) // .describedAs(testQuery) // @@ -833,7 +840,7 @@ class StringQueryUnitTests { void questionMarkInStringLiteral() { String queryString = "select '? ' from dual"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.getQueryString()).isEqualTo(queryString); assertThat(query.hasParameterBindings()).isFalse(); @@ -853,7 +860,7 @@ class StringQueryUnitTests { "select a, b from C"); for (String queryString : queriesWithoutDefaultProjection) { - assertThat(new StringQuery(queryString, true).isDefaultProjection()) // + assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) // .describedAs(queryString) // .isFalse(); } @@ -870,7 +877,7 @@ class StringQueryUnitTests { ); for (String queryString : queriesWithDefaultProjection) { - assertThat(new StringQuery(queryString, true).isDefaultProjection()) // + assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) // .describedAs(queryString) // .isTrue(); } @@ -880,7 +887,7 @@ class StringQueryUnitTests { void questionMarkInStringLiteralWithParameters() { String queryString = "SELECT CAST(REGEXP_SUBSTR(itp.template_as_txt, '(?<=templateId\\\\\\\\=)(\\\\\\\\d+)(?:\\\\\\\\R)') AS INT) AS templateId FROM foo itp WHERE bar = ?1 AND baz = 1"; - StringQuery query = new StringQuery(queryString, false); + DefaultEntityQuery query = new TestEntityQuery(queryString, false); assertThat(query.getQueryString()).isEqualTo(queryString); assertThat(query.hasParameterBindings()).isTrue(); @@ -892,7 +899,7 @@ class StringQueryUnitTests { void usingPipesWithNamedParameter() { String queryString = "SELECT u FROM User u WHERE u.lastname LIKE '%'||:name||'%'"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.getParameterBindings()) // .extracting(ParameterBinding::getName) // @@ -903,7 +910,7 @@ class StringQueryUnitTests { void usingGreaterThanWithNamedParameter() { String queryString = "SELECT u FROM User u WHERE :age>u.age"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(query.getParameterBindings()) // .extracting(ParameterBinding::getName) // @@ -912,9 +919,8 @@ class StringQueryUnitTests { void checkNumberOfNamedParameters(String query, int expectedSize, String label, boolean nativeQuery) { - EntityQuery introspectedQuery = nativeQuery - ? EntityQuery.introspectNativeQuery(query, QueryEnhancerSelector.DEFAULT_SELECTOR) - : EntityQuery.introspectJpql(query, QueryEnhancerSelector.DEFAULT_SELECTOR); + DeclaredQuery declaredQuery = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); + EntityQuery introspectedQuery = EntityQuery.create(declaredQuery, QueryEnhancerSelector.DEFAULT_SELECTOR); assertThat(introspectedQuery.hasNamedParameter()) // .describedAs("hasNamed Parameter " + label) // @@ -927,7 +933,8 @@ class StringQueryUnitTests { private void checkHasNamedParameter(String query, boolean expected, String label) { DeclaredQuery source = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); - BindableQuery bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); + PreprocessedQuery bindableQuery = PreprocessedQuery.ParameterBindingParser.INSTANCE.parse(source.getQueryString(), + source::rewrite, it -> {}); assertThat(bindableQuery.getBindings().stream().anyMatch(it -> it.getIdentifier().hasName())) // .describedAs(String.format("<%s> (%s)", query, label)) // diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java index 9a5c9ff30..7dd6dd757 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java @@ -21,6 +21,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; /** * TCK Tests for {@link DefaultQueryEnhancer}. @@ -45,7 +47,8 @@ class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests { QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery("SELECT e FROM Employee e")); - String sql = enhancer.applySorting(Sort.by("foo", "bar")); + String sql = enhancer.rewrite(new DefaultQueryRewriteInformation(Sort.by("foo", "bar"), + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))); assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc"); } @@ -53,9 +56,11 @@ class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Test // GH-3811 void shouldApplySortingWithNullHandling() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery("SELECT e FROM Employee e")); - String sql = enhancer.applySorting(Sort.by(Sort.Order.asc("foo").nullsFirst(), Sort.Order.asc("bar").nullsLast())); + String sql = enhancer.rewrite(new DefaultQueryRewriteInformation( + Sort.by(Sort.Order.asc("foo").nullsFirst(), Sort.Order.asc("bar").nullsLast()), + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))); assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc nulls first, e.bar asc nulls last"); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java index 5303378b8..dbe4d45a9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java @@ -30,7 +30,7 @@ public class EqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override QueryEnhancer createQueryEnhancer(DeclaredQuery query) { - assumeThat(query.isNativeQuery()).isFalse(); + assumeThat(query.isNative()).isFalse(); return JpaQueryEnhancer.forEql(query.getQueryString()); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java index 61436aae5..8f9385969 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java @@ -29,6 +29,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; /** * Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the @@ -221,7 +223,9 @@ class EqlQueryTransformerTests { where exists (select u2 from user u2 ) - """).applySorting(sort)).isEqualToIgnoringWhitespace(""" + """).rewrite(new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) + .isEqualToIgnoringWhitespace(""" select u from user u where exists (select u2 @@ -803,7 +807,8 @@ class EqlQueryTransformerTests { } private String createQueryFor(String query, Sort sort) { - return newParser(query).applySorting(sort); + return newParser(query).rewrite(new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))); } private String createCountQueryFor(String query) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java index 916db5e06..f25e9fc2e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java @@ -30,7 +30,7 @@ public class HqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override QueryEnhancer createQueryEnhancer(DeclaredQuery query) { - assumeThat(query.isNativeQuery()).isFalse(); + assumeThat(query.isNative()).isFalse(); return JpaQueryEnhancer.forHql(query.getQueryString()); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java index d9634ea91..cd2c3987f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java @@ -33,6 +33,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.StringUtils; /** @@ -280,7 +282,9 @@ class HqlQueryTransformerTests { where exists (select u2 from user u2 ) - """).applySorting(sort)).isEqualToIgnoringWhitespace(""" + """).rewrite(new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) + .isEqualToIgnoringWhitespace(""" select u from user u where exists (select u2 @@ -1172,7 +1176,8 @@ class HqlQueryTransformerTests { } private String createQueryFor(String query, Sort sort) { - return newParser(query).applySorting(sort); + return newParser(query).rewrite(new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))); } private String createCountQueryFor(String query) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java index 6279919b3..bc2e0236b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java @@ -48,7 +48,8 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.jpqlQuery("SELECT e FROM Employee e")); - String sql = enhancer.applySorting(Sort.by("foo", "bar")); + String sql = enhancer.rewrite(new DefaultQueryRewriteInformation(Sort.by("foo", "bar"), + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))); assertThat(sql).isEqualTo("SELECT e FROM Employee e ORDER BY e.foo ASC, e.bar ASC"); } @@ -77,7 +78,7 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { ORDER BY b.b1, a.a1, a.a2 """)); - String sql = enhancer.createCountQueryFor(); + String sql = enhancer.createCountQueryFor(null); assertThat(sql).startsWith("SELECT count(DISTINCT a.*) FROM TableA a"); } @@ -97,16 +98,16 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { + "except \n" // + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE"; - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); + DefaultEntityQuery query = new TestEntityQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancer.create(query); - assertThat(stringQuery.getAlias()).isNullOrEmpty(); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); + assertThat(query.getAlias()).isNullOrEmpty(); + assertThat(query.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); + assertThat(query.hasConstructorExpression()).isFalse(); - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); - assertThat(queryEnhancer.applySorting(Sort.by("SOME_COLUMN"))).endsWith("ORDER BY SOME_COLUMN ASC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(setQuery); + assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("SOME_COLUMN")))) + .endsWith("ORDER BY SOME_COLUMN ASC"); assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -120,16 +121,16 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE \n" // + "union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"; - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); + DefaultEntityQuery query = new TestEntityQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); - assertThat(stringQuery.getAlias()).isNullOrEmpty(); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); + assertThat(query.getAlias()).isNullOrEmpty(); + assertThat(query.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); + assertThat(query.hasConstructorExpression()).isFalse(); - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); - assertThat(queryEnhancer.applySorting(Sort.by("SOME_COLUMN").ascending())).endsWith("ORDER BY SOME_COLUMN ASC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(setQuery); + assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("SOME_COLUMN").ascending()))) + .endsWith("ORDER BY SOME_COLUMN ASC"); assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -147,16 +148,16 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { + "\tselect CustomerID from customers where country = 'Germany'\n"// + "\t;"; - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); + DefaultEntityQuery query = new TestEntityQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); - assertThat(stringQuery.getAlias()).isNullOrEmpty(); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("CustomerID"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); + assertThat(query.getAlias()).isNullOrEmpty(); + assertThat(query.getProjection()).isEqualToIgnoringCase("CustomerID"); + assertThat(query.hasConstructorExpression()).isFalse(); - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); - assertThat(queryEnhancer.applySorting(Sort.by("CustomerID").descending())).endsWith("ORDER BY CustomerID DESC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(setQuery); + assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("CustomerID").descending()))) + .endsWith("ORDER BY CustomerID DESC"); assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("CustomerID"); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -167,16 +168,15 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { String setQuery = "VALUES (1, 2, 'test')"; - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); + DefaultEntityQuery query = new TestEntityQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); - assertThat(stringQuery.getAlias()).isNullOrEmpty(); - assertThat(stringQuery.getProjection()).isNullOrEmpty(); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); + assertThat(query.getAlias()).isNullOrEmpty(); + assertThat(query.getProjection()).isNullOrEmpty(); + assertThat(query.hasConstructorExpression()).isFalse(); - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); - assertThat(queryEnhancer.applySorting(Sort.by("CustomerID").descending())).isEqualTo(setQuery); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(setQuery); + assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("CustomerID").descending()))).isEqualTo(setQuery); assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); assertThat(queryEnhancer.getProjection()).isNullOrEmpty(); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -188,18 +188,18 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { String setQuery = "with sample_data(day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))) \n" + "select day, value from sample_data as a"; - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); + DefaultEntityQuery query = new TestEntityQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); - assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); + assertThat(query.getAlias()).isEqualToIgnoringCase("a"); + assertThat(query.getProjection()).isEqualToIgnoringCase("day, value"); + assertThat(query.hasConstructorExpression()).isFalse(); - assertThat(queryEnhancer.createCountQueryFor().toLowerCase()).isEqualToIgnoringWhitespace( + assertThat(queryEnhancer.createCountQueryFor(null).toLowerCase()).isEqualToIgnoringWhitespace( "with sample_data (day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))) " + "select count(1) from sample_data as a"); - assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).endsWith("ORDER BY a.day DESC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("day").descending()))) + .endsWith("ORDER BY a.day DESC"); assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase("a"); assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("day, value"); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -211,18 +211,18 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { String setQuery = "with sample_data(day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))), test2 as (values (1,2,3)) \n" + "select day, value from sample_data as a"; - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); + DefaultEntityQuery query = new TestEntityQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); - assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); + assertThat(query.getAlias()).isEqualToIgnoringCase("a"); + assertThat(query.getProjection()).isEqualToIgnoringCase("day, value"); + assertThat(query.hasConstructorExpression()).isFalse(); - assertThat(queryEnhancer.createCountQueryFor().toLowerCase()).isEqualToIgnoringWhitespace( + assertThat(queryEnhancer.createCountQueryFor(null).toLowerCase()).isEqualToIgnoringWhitespace( "with sample_data (day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))), test2 as (values (1, 2, 3)) " + "select count(1) from sample_data as a"); - assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).endsWith("ORDER BY a.day DESC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("day").descending()))) + .endsWith("ORDER BY a.day DESC"); assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase("a"); assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("day, value"); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -231,15 +231,15 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Test // GH-3038 void truncateStatementShouldWork() { - StringQuery stringQuery = new StringQuery("TRUNCATE TABLE foo", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); + DefaultEntityQuery query = new TestEntityQuery("TRUNCATE TABLE foo", true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); - assertThat(stringQuery.getAlias()).isNull(); - assertThat(stringQuery.getProjection()).isEmpty(); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); + assertThat(query.getAlias()).isNull(); + assertThat(query.getProjection()).isEmpty(); + assertThat(query.hasConstructorExpression()).isFalse(); - assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).isEqualTo("TRUNCATE TABLE foo"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("day").descending()))) + .isEqualTo("TRUNCATE TABLE foo"); assertThat(queryEnhancer.detectAlias()).isNull(); assertThat(queryEnhancer.getProjection()).isEmpty(); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -247,15 +247,14 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @ParameterizedTest // GH-2641 @MethodSource("mergeStatementWorksSource") - void mergeStatementWorksWithJSqlParser(String query, String alias) { + void mergeStatementWorksWithJSqlParser(String queryString, String alias) { - StringQuery stringQuery = new StringQuery(query, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); - assertThat(QueryUtils.detectAlias(query)).isNull(); + assertThat(QueryUtils.detectAlias(queryString)).isNull(); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); assertThat(queryEnhancer.getProjection()).isEmpty(); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -285,4 +284,9 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { assertThat(queryEnhancer.getProjection()).isEqualTo("is_contained_in(:innerId, :outerId)"); } + private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) { + return new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); + } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java index 32f9e965a..44256fe4c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java @@ -30,7 +30,7 @@ public class JpqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override QueryEnhancer createQueryEnhancer(DeclaredQuery query) { - assumeThat(query.isNativeQuery()).isFalse(); + assumeThat(query.isNative()).isFalse(); return JpaQueryEnhancer.forJpql(query.getQueryString()); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java index 1a38f729e..39ed9b6d9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java @@ -29,6 +29,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; /** * Verify that JPQL queries are properly transformed through the {@link JpaQueryEnhancer} and the @@ -216,13 +218,16 @@ class JpqlQueryTransformerTests { Sort sort = Sort.by(Sort.Order.desc("age")); + assertThat(newParser(""" select u from user u where exists (select u2 from user u2 ) - """).applySorting(sort)).isEqualToIgnoringWhitespace(""" + """).rewrite(new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) + .isEqualToIgnoringWhitespace(""" select u from user u where exists (select u2 @@ -808,7 +813,8 @@ class JpqlQueryTransformerTests { } private String createQueryFor(String query, Sort sort) { - return newParser(query).applySorting(sort); + return newParser(query).rewrite(new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))); } private String createCountQueryFor(String query) { 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 fa44d2ca1..c17cc49f9 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 @@ -30,7 +30,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; @@ -71,14 +70,14 @@ class NativeJpaQueryUnitTests { JpaQueryMethod queryMethod = new JpaQueryMethod(respositoryMethod, repositoryMetadata, projectionFactory, queryExtractor); - Query annotation = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class); - - NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, annotation.value(), annotation.countQuery(), + NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, queryMethod.getRequiredDeclaredQuery(), + queryMethod.getDeclaredCountQuery(), new JpaQueryConfiguration(QueryRewriterProvider.simple(), QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT)); - String sql = query.getSortedQueryString(Sort.by("foo", "bar"), queryMethod.getResultProcessor().getReturnedType()); + QueryProvider sql = query.getSortedQuery(Sort.by("foo", "bar"), + queryMethod.getResultProcessor().getReturnedType()); - assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc"); + assertThat(sql.getQueryString()).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc"); } interface TestRepo extends Repository { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java index edcaf0e4e..f765860a2 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java @@ -18,7 +18,6 @@ package org.springframework.data.jpa.repository.query; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.springframework.data.jpa.repository.query.StringQuery.ParameterBindingParser; /** * Unit tests for the {@link ParameterBindingParser}. @@ -68,7 +67,7 @@ class ParameterBindingParserUnitTests { private void checkHasParameter(SoftAssertions softly, String query, boolean containsParameter, String label) { - StringQuery stringQuery = new StringQuery(query, false); + DefaultEntityQuery stringQuery = new TestEntityQuery(query, false); softly.assertThat(stringQuery.getParameterBindings().size()) // .describedAs(String.format("<%s> (%s)", query, label)) // diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java index aaccc4cad..2f5234121 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java @@ -32,9 +32,10 @@ class QueryEnhancerFactoryUnitTests { @Test void createsParsingImplementationForNonNativeQuery() { - StringQuery query = new StringQuery("select new com.example.User(u.firstname) from User u", false); + DefaultEntityQuery query = new TestEntityQuery("select new com.example.User(u.firstname) from User u", + false); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query); + QueryEnhancer queryEnhancer = QueryEnhancer.create(query); assertThat(queryEnhancer) // .isInstanceOf(JpaQueryEnhancer.class); @@ -47,9 +48,9 @@ class QueryEnhancerFactoryUnitTests { @Test void createsJSqlImplementationForNativeQuery() { - StringQuery query = new StringQuery("select * from User", true); + DefaultEntityQuery query = new TestEntityQuery("select * from User", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); assertThat(queryEnhancer) // .isInstanceOf(JSqlParserQueryEnhancer.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java index 7a0f4e178..98e19b6cb 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java @@ -36,7 +36,7 @@ abstract class QueryEnhancerTckTests { void shouldDeriveNativeCountQuery(String query, String expected) { QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(query)); - String countQueryFor = enhancer.createCountQueryFor(); + String countQueryFor = enhancer.createCountQueryFor(null); // lenient cleanup to allow for rendering variance String sanitized = countQueryFor.replaceAll("\r", " ").replaceAll("\n", " ").replaceAll(" {2}", " ") @@ -179,7 +179,7 @@ abstract class QueryEnhancerTckTests { void shouldDeriveNativeCountQueryWithVariable(String query, String expected) { QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(query)); - String countQueryFor = enhancer.createCountQueryFor(); + String countQueryFor = enhancer.createCountQueryFor(null); assertThat(countQueryFor).isEqualToIgnoringCase(expected); } @@ -203,9 +203,9 @@ abstract class QueryEnhancerTckTests { // DATAJPA-1696 void findProjectionClauseWithIncludedFrom() { - StringQuery query = new StringQuery("select x, frommage, y from t", true); + DefaultEntityQuery query = new TestEntityQuery("select x, frommage, y from t", true); - assertThat(createQueryEnhancer(query.getDeclaredQuery()).getProjection()).isEqualTo("x, frommage, y"); + assertThat(createQueryEnhancer(query).getProjection()).isEqualTo("x, frommage, y"); } abstract QueryEnhancer createQueryEnhancer(DeclaredQuery query); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java index 0e5f44cd8..da113f567 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java @@ -20,7 +20,6 @@ import static org.assertj.core.api.Assumptions.*; import java.util.Arrays; import java.util.Collections; -import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -30,9 +29,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; /** * Unit tests for {@link QueryEnhancer}. @@ -40,6 +42,7 @@ import org.springframework.data.jpa.domain.JpaSort; * @author Diego Krupitza * @author Geoffrey Deremetz * @author Krzysztof Krason + * @author Mark Paluch */ class QueryEnhancerUnitTests { @@ -78,7 +81,7 @@ class QueryEnhancerUnitTests { @ParameterizedTest @MethodSource("detectsAliasWithUCorrectlySource") - void detectsAliasWithUCorrectly(IntrospectedQuery query, String alias) { + void detectsAliasWithUCorrectly(DefaultEntityQuery query, String alias) { assumeThat(query.getQueryString()).as("JsqlParser does not support simple JPA syntax") .doesNotStartWithIgnoringCase("from"); @@ -89,21 +92,21 @@ class QueryEnhancerUnitTests { public static Stream detectsAliasWithUCorrectlySource() { return Stream.of( // - Arguments.of(new StringQuery(QUERY, true), "u"), // - Arguments.of(new StringQuery(SIMPLE_QUERY, false), "u"), // - Arguments.of(new StringQuery(COUNT_QUERY, true), "u"), // - Arguments.of(new StringQuery(QUERY_WITH_AS, true), "u"), // - Arguments.of(new StringQuery("SELECT u FROM USER U", false), "U"), // - Arguments.of(new StringQuery("select u from User u", true), "u"), // - Arguments.of(new StringQuery("select u from com.acme.User u", true), "u"), // - Arguments.of(new StringQuery("select u from T05User u", true), "u") // + Arguments.of(new TestEntityQuery(QUERY, true), "u"), // + Arguments.of(new TestEntityQuery(SIMPLE_QUERY, false), "u"), // + Arguments.of(new TestEntityQuery(COUNT_QUERY, true), "u"), // + Arguments.of(new TestEntityQuery(QUERY_WITH_AS, true), "u"), // + Arguments.of(new TestEntityQuery("SELECT u FROM USER U", false), "U"), // + Arguments.of(new TestEntityQuery("select u from User u", true), "u"), // + Arguments.of(new TestEntityQuery("select u from com.acme.User u", true), "u"), // + Arguments.of(new TestEntityQuery("select u from T05User u", true), "u") // ); } @Test void allowsFullyQualifiedEntityNamesInQuery() { - StringQuery query = new StringQuery(FQ_QUERY, true); + DefaultEntityQuery query = new TestEntityQuery(FQ_QUERY, true); assertThat(getEnhancer(query).detectAlias()).isEqualTo("u"); assertCountQuery(FQ_QUERY, "select count(u) from org.acme.domain.User$Foo_Bar u", true); @@ -112,20 +115,18 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-252 void doesNotPrefixOrderReferenceIfOuterJoinAliasDetected() { - StringQuery query = new StringQuery("select p from Person p left join p.address address", true); + DefaultEntityQuery query = new TestEntityQuery("select p from Person p left join p.address address", true); - assertThat(getEnhancer(query).applySorting(Sort.by("address.city"))) + assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("address.city")))) .endsWithIgnoringCase("order by address.city asc"); - assertThat(getEnhancer(query).applySorting(Sort.by("address.city", "lastname"), "p")) - .endsWithIgnoringCase("order by address.city asc, p.lastname asc"); } @Test // DATAJPA-252 void extendsExistingOrderByClausesCorrectly() { - StringQuery query = new StringQuery("select p from Person p order by p.lastname asc", true); + DefaultEntityQuery query = new TestEntityQuery("select p from Person p order by p.lastname asc", true); - assertThat(getEnhancer(query).applySorting(Sort.by("firstname"), "p")) + assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("firstname")))) .endsWithIgnoringCase("order by p.lastname asc, p.firstname asc"); } @@ -134,9 +135,10 @@ class QueryEnhancerUnitTests { Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase()); - StringQuery query = new StringQuery("select p from Person p", true); + DefaultEntityQuery query = new TestEntityQuery("select p from Person p", true); - assertThat(getEnhancer(query).applySorting(sort, "p")).endsWithIgnoringCase("order by lower(p.firstname) asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) + .endsWithIgnoringCase("order by lower(p.firstname) asc"); } @Test // DATAJPA-296 @@ -144,9 +146,9 @@ class QueryEnhancerUnitTests { Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase()); - StringQuery query = new StringQuery("select p from Person p order by p.lastname asc", true); + DefaultEntityQuery query = new TestEntityQuery("select p from Person p order by p.lastname asc", true); - assertThat(getEnhancer(query).applySorting(sort, "p")) + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) .endsWithIgnoringCase("order by p.lastname asc, lower(p.firstname) asc"); } @@ -160,12 +162,12 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-148 void doesNotPrefixSortsIfFunction() { - StringQuery query = new StringQuery("select p from Person p", true); + DefaultEntityQuery query = new TestEntityQuery("select p from Person p", true); Sort sort = Sort.by("sum(foo)"); QueryEnhancer enhancer = getEnhancer(query); - assertThatThrownBy(() -> enhancer.applySorting(sort, "p")) // + assertThatThrownBy(() -> enhancer.rewrite(getRewriteInformation(sort))) // .isInstanceOf(InvalidDataAccessApiUsageException.class); } @@ -173,8 +175,8 @@ class QueryEnhancerUnitTests { void findsExistingOrderByIndependentOfCase() { Sort sort = Sort.by("lastname"); - StringQuery originalQuery = new StringQuery("select p from Person p ORDER BY p.firstname", true); - String query = getEnhancer(originalQuery).applySorting(sort, "p"); + DefaultEntityQuery originalQuery = new TestEntityQuery("select p from Person p ORDER BY p.firstname", true); + String query = getEnhancer(originalQuery).rewrite(getRewriteInformation(sort)); assertThat(query).endsWithIgnoringCase("ORDER BY p.firstname, p.lastname asc"); } @@ -182,17 +184,17 @@ class QueryEnhancerUnitTests { @Test // GH-3263 void preserveSourceQueryWhenAddingSort() { - StringQuery query = new StringQuery("WITH all_projects AS (SELECT * FROM projects) SELECT * FROM all_projects p", - true); + DefaultEntityQuery query = new TestEntityQuery( + "WITH all_projects AS (SELECT * FROM projects) SELECT * FROM all_projects p", true); - assertThat(getEnhancer(query).applySorting(Sort.by("name"), "p")) // + assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("name")))) // .startsWithIgnoringCase(query.getQueryString()).endsWithIgnoringCase("ORDER BY p.name ASC"); } @Test // GH-2812 void createCountQueryFromDeleteQuery() { - StringQuery query = new StringQuery("delete from some_table where id in :ids", true); + DefaultEntityQuery query = new TestEntityQuery("delete from some_table where id in :ids", true); assertThat(getEnhancer(query).createCountQueryFor("p.lastname")) .isEqualToIgnoringCase("delete from some_table where id in :ids"); @@ -201,7 +203,7 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-456 void createCountQueryFromTheGivenCountProjection() { - StringQuery query = new StringQuery("select p.lastname,p.firstname from Person p", true); + DefaultEntityQuery query = new TestEntityQuery("select p.lastname,p.firstname from Person p", true); assertThat(getEnhancer(query).createCountQueryFor("p.lastname")) .isEqualToIgnoringCase("select count(p.lastname) from Person p"); @@ -210,24 +212,26 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-726 void detectsAliasesInPlainJoins() { - StringQuery query = new StringQuery("select p from Customer c join c.productOrder p where p.delay = true", true); + DefaultEntityQuery query = new TestEntityQuery( + "select p from Customer c join c.productOrder p where p.delay = true", true); Sort sort = Sort.by("p.lineItems"); - assertThat(getEnhancer(query).applySorting(sort, "c")).endsWithIgnoringCase("order by p.lineItems asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) + .endsWithIgnoringCase("order by p.lineItems asc"); } @Test // DATAJPA-736 void supportsNonAsciiCharactersInEntityNames() { - StringQuery query = new StringQuery("select u from Usèr u", true); + DefaultEntityQuery query = new TestEntityQuery("select u from Usèr u", true); - assertThat(getEnhancer(query).createCountQueryFor()).isEqualToIgnoringCase("select count(u) from Usèr u"); + assertThat(getEnhancer(query).createCountQueryFor(null)).isEqualToIgnoringCase("select count(u) from Usèr u"); } @Test // DATAJPA-798 void detectsAliasInQueryContainingLineBreaks() { - StringQuery query = new StringQuery("select \n u \n from \n User \nu", true); + DefaultEntityQuery query = new TestEntityQuery("select \n u \n from \n User \nu", true); assertThat(getEnhancer(query).detectAlias()).isEqualTo("u"); } @@ -236,26 +240,28 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-815 void doesPrefixPropertyWithNonNative() { - StringQuery query = new StringQuery("from Cat c join Dog d", false); + DefaultEntityQuery query = new TestEntityQuery("from Cat c join Dog d", false); Sort sort = Sort.by("dPropertyStartingWithJoinAlias"); - assertThat(getEnhancer(query).applySorting(sort, "c")).endsWith("order by c.dPropertyStartingWithJoinAlias asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) + .endsWith("order by c.dPropertyStartingWithJoinAlias asc"); } @Test // DATAJPA-815 void doesPrefixPropertyWithNative() { - StringQuery query = new StringQuery("Select * from Cat c join Dog d", true); + DefaultEntityQuery query = new TestEntityQuery("Select * from Cat c join Dog d", true); Sort sort = Sort.by("dPropertyStartingWithJoinAlias"); - assertThat(getEnhancer(query).applySorting(sort, "c")) + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) .endsWithIgnoringCase("order by c.dPropertyStartingWithJoinAlias asc"); } @Test // DATAJPA-938 void detectsConstructorExpressionInDistinctQuery() { - StringQuery query = new StringQuery("select distinct new com.example.Foo(b.name) from Bar b", false); + DefaultEntityQuery query = new TestEntityQuery("select distinct new com.example.Foo(b.name) from Bar b", + false); assertThat(getEnhancer(query).hasConstructorExpression()).isTrue(); } @@ -263,7 +269,7 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-938 void detectsComplexConstructorExpression() { - StringQuery query = new StringQuery("select new foo.bar.Foo(ip.id, ip.name, sum(lp.amount)) " // + DefaultEntityQuery query = new TestEntityQuery("select new foo.bar.Foo(ip.id, ip.name, sum(lp.amount)) " // + "from Bar lp join lp.investmentProduct ip " // + "where (lp.toDate is null and lp.fromDate <= :now and lp.fromDate is not null) and lp.accountId = :accountId " // @@ -276,7 +282,7 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-938 void detectsConstructorExpressionWithLineBreaks() { - StringQuery query = new StringQuery("select new foo.bar.FooBar(\na.id) from DtoA a ", false); + DefaultEntityQuery query = new TestEntityQuery("select new foo.bar.FooBar(\na.id) from DtoA a ", false); assertThat(getEnhancer(query).hasConstructorExpression()).isTrue(); } @@ -285,140 +291,138 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-960 void doesNotQualifySortIfNoAliasDetectedNonNative() { - StringQuery query = new StringQuery("from mytable where ?1 is null", false); + DefaultEntityQuery query = new TestEntityQuery("from mytable where ?1 is null", false); - assertThat(getEnhancer(query).applySorting(Sort.by("firstname"))).endsWith("order by firstname asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("firstname")))) + .endsWith("order by firstname asc"); } @Test // DATAJPA-960 void doesNotQualifySortIfNoAliasDetectedNative() { - StringQuery query = new StringQuery("Select * from mytable where ?1 is null", true); + DefaultEntityQuery query = new TestEntityQuery("Select * from mytable where ?1 is null", true); - assertThat(getEnhancer(query).applySorting(Sort.by("firstname"))).endsWithIgnoringCase("order by firstname asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("firstname")))) + .endsWithIgnoringCase("order by firstname asc"); } @Test // DATAJPA-965, DATAJPA-970 void doesNotAllowWhitespaceInSort() { - StringQuery query = new StringQuery("select p from Person p", true); + DefaultEntityQuery query = new TestEntityQuery("select p from Person p", true); Sort sort = Sort.by("case when foo then bar"); assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> getEnhancer(query).applySorting(sort, "p")); + .isThrownBy(() -> getEnhancer(query).rewrite(getRewriteInformation(sort))); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixUnsafeJpaSortFunctionCalls() { JpaSort sort = JpaSort.unsafe("sum(foo)"); - StringQuery query = new StringQuery("select p from Person p", true); + DefaultEntityQuery query = new TestEntityQuery("select p from Person p", true); - assertThat(getEnhancer(query).applySorting(sort, "p")).endsWithIgnoringCase("order by sum(foo) asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWithIgnoringCase("order by sum(foo) asc"); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixMultipleAliasedFunctionCalls() { - StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m", - true); + DefaultEntityQuery query = new TestEntityQuery( + "SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m", true); Sort sort = Sort.by("avgPrice", "sumStocks"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by avgPrice asc, sumStocks asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) + .endsWithIgnoringCase("order by avgPrice asc, sumStocks asc"); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixSingleAliasedFunctionCalls() { - StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true); + DefaultEntityQuery query = new TestEntityQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true); Sort sort = Sort.by("avgPrice"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by avgPrice asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWithIgnoringCase("order by avgPrice asc"); } @Test // DATAJPA-965, DATAJPA-970 void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() { - StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true); + DefaultEntityQuery query = new TestEntityQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true); Sort sort = Sort.by("someOtherProperty"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by m.someOtherProperty asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) + .endsWithIgnoringCase("order by m.someOtherProperty asc"); } @Test // DATAJPA-965, DATAJPA-970 void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() { - StringQuery query = new StringQuery("SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m", true); + DefaultEntityQuery query = new TestEntityQuery("SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m", + true); Sort sort = Sort.by("name", "avgPrice"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by m.name asc, avgPrice asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) + .endsWithIgnoringCase("order by m.name asc, avgPrice asc"); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixAliasedFunctionCallNameWithMultipleNumericParameters() { - StringQuery query = new StringQuery("SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m", true); + DefaultEntityQuery query = new TestEntityQuery( + "SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m", true); Sort sort = Sort.by("trimmedName"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by trimmedName asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) + .endsWithIgnoringCase("order by trimmedName asc"); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixAliasedFunctionCallNameWithMultipleStringParameters() { - StringQuery query = new StringQuery("SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m", true); + DefaultEntityQuery query = new TestEntityQuery( + "SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m", true); Sort sort = Sort.by("extendedName"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by extendedName asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))) + .endsWithIgnoringCase("order by extendedName asc"); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixAliasedFunctionCallNameWithUnderscores() { - StringQuery query = new StringQuery("SELECT AVG(m.price) AS avg_price FROM Magazine m", true); + DefaultEntityQuery query = new TestEntityQuery("SELECT AVG(m.price) AS avg_price FROM Magazine m", true); Sort sort = Sort.by("avg_price"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by avg_price asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWithIgnoringCase("order by avg_price asc"); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixAliasedFunctionCallNameWithDots() { - StringQuery query = new StringQuery("SELECT AVG(m.price) AS average FROM Magazine m", false); + DefaultEntityQuery query = new TestEntityQuery("SELECT AVG(m.price) AS average FROM Magazine m", false); Sort sort = Sort.by("avg"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWith("order by m.avg asc"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWith("order by m.avg asc"); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixAliasedFunctionCallNameWithDotsNativeQuery() { // this is invalid since the '.' character is not allowed. Not in sql nor in JPQL. - assertThatThrownBy(() -> new StringQuery("SELECT AVG(m.price) AS m.avg FROM Magazine m", true)) // + assertThatThrownBy(() -> new TestEntityQuery("SELECT AVG(m.price) AS m.avg FROM Magazine m", true)) // .isInstanceOf(IllegalArgumentException.class); } @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() { - StringQuery query = new StringQuery("SELECT AVG( m.price ) AS avgPrice FROM Magazine m", true); + DefaultEntityQuery query = new TestEntityQuery( + "SELECT AVG( m.price ) AS avgPrice FROM Magazine m", true); Sort sort = Sort.by("avgPrice"); - assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by avgPrice asc"); - } - - @Test // DATAJPA-1000 - void discoversCorrectAliasForJoinFetch() { - - String queryString = "SELECT DISTINCT user FROM User user LEFT JOIN user.authorities AS authority"; - Set aliases = QueryUtils.getOuterJoinAliases(queryString); - - StringQuery nativeQuery = new StringQuery(queryString, true); - Set joinAliases = new JSqlParserQueryEnhancer(nativeQuery).getJoinAliases(); - - assertThat(aliases).containsExactly("authority"); - assertThat(joinAliases).containsExactly("authority"); + assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWithIgnoringCase("order by avgPrice asc"); } @Test // DATAJPA-1171 @@ -438,11 +442,11 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-1506 void detectsAliasWithGroupAndOrderBy() { - StringQuery queryWithGroupNoAlias = new StringQuery("select * from User group by name", true); - StringQuery queryWithGroupAlias = new StringQuery("select * from User u group by name", true); + DefaultEntityQuery queryWithGroupNoAlias = new TestEntityQuery("select * from User group by name", true); + DefaultEntityQuery queryWithGroupAlias = new TestEntityQuery("select * from User u group by name", true); - StringQuery queryWithOrderNoAlias = new StringQuery("select * from User order by name", true); - StringQuery queryWithOrderAlias = new StringQuery("select * from User u order by name", true); + DefaultEntityQuery queryWithOrderNoAlias = new TestEntityQuery("select * from User order by name", true); + DefaultEntityQuery queryWithOrderAlias = new TestEntityQuery("select * from User u order by name", true); assertThat(getEnhancer(queryWithGroupNoAlias).detectAlias()).isNull(); assertThat(getEnhancer(queryWithOrderNoAlias).detectAlias()).isNull(); @@ -453,12 +457,12 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-1061 void appliesSortCorrectlyForFieldAliases() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a", true); Sort sort = Sort.by("authorName"); - String fullQuery = getEnhancer(query).applySorting(sort); + String fullQuery = getEnhancer(query).rewrite(getRewriteInformation(sort)); assertThat(fullQuery).endsWithIgnoringCase("order by authorName asc"); } @@ -466,11 +470,11 @@ class QueryEnhancerUnitTests { @Test // GH-2280 void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() { - StringQuery query = new StringQuery("SELECT customer.id as id, customer.name as name FROM CustomerEntity customer", - true); + DefaultEntityQuery query = new TestEntityQuery( + "SELECT customer.id as id, customer.name as name FROM CustomerEntity customer", true); Sort sort = Sort.by(Sort.Order.by("name").ignoreCase()); - String fullQuery = getEnhancer(query).applySorting(sort); + String fullQuery = getEnhancer(query).rewrite(getRewriteInformation(sort)); assertThat(fullQuery).isEqualToIgnoringCase( "SELECT customer.id as id, customer.name as name FROM CustomerEntity customer order by lower(name) asc"); @@ -479,12 +483,12 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-1061 void appliesSortCorrectlyForFunctionAliases() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a", true); Sort sort = Sort.by("title"); - String fullQuery = getEnhancer(query).applySorting(sort); + String fullQuery = getEnhancer(query).rewrite(getRewriteInformation(sort)); assertThat(fullQuery).endsWithIgnoringCase("order by title asc"); } @@ -492,12 +496,12 @@ class QueryEnhancerUnitTests { @Test // DATAJPA-1061 void appliesSortCorrectlyForSimpleField() { - StringQuery query = new StringQuery( + DefaultEntityQuery query = new TestEntityQuery( "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a", true); Sort sort = Sort.by("price"); - String fullQuery = getEnhancer(query).applySorting(sort); + String fullQuery = getEnhancer(query).rewrite(getRewriteInformation(sort)); assertThat(fullQuery).endsWithIgnoringCase("order by m.price asc"); } @@ -505,30 +509,34 @@ class QueryEnhancerUnitTests { @Test void createCountQuerySupportsLineBreakRightAfterDistinct() { - StringQuery query1 = new StringQuery("select\ndistinct\nuser.age,\n" + // + DefaultEntityQuery query1 = new TestEntityQuery("select\ndistinct\nuser.age,\n" + // "user.name\n" + // "from\nUser\nuser", true); - StringQuery query2 = new StringQuery("select\ndistinct user.age,\n" + // + DefaultEntityQuery query2 = new TestEntityQuery("select\ndistinct user.age,\n" + // "user.name\n" + // "from\nUser\nuser", true); - assertThat(getEnhancer(query1).createCountQueryFor()).isEqualTo(getEnhancer(query2).createCountQueryFor()); + assertThat(getEnhancer(query1).createCountQueryFor(null)).isEqualTo(getEnhancer(query2).createCountQueryFor(null)); } @Test void detectsAliasWithGroupAndOrderByWithLineBreaks() { - StringQuery queryWithGroupAndLineBreak = new StringQuery("select * from User group\nby name", true); - StringQuery queryWithGroupAndLineBreakAndAlias = new StringQuery("select * from User u group\nby name", true); + DefaultEntityQuery queryWithGroupAndLineBreak = new TestEntityQuery("select * from User group\nby name", + true); + DefaultEntityQuery queryWithGroupAndLineBreakAndAlias = new TestEntityQuery( + "select * from User u group\nby name", true); assertThat(getEnhancer(queryWithGroupAndLineBreak).detectAlias()).isNull(); assertThat(getEnhancer(queryWithGroupAndLineBreakAndAlias).detectAlias()).isEqualTo("u"); - StringQuery queryWithOrderAndLineBreak = new StringQuery("select * from User order\nby name", true); - StringQuery queryWithOrderAndLineBreakAndAlias = new StringQuery("select * from User u order\nby name", true); - StringQuery queryWithOrderAndMultipleLineBreakAndAlias = new StringQuery("select * from User\nu\norder \n by name", + DefaultEntityQuery queryWithOrderAndLineBreak = new TestEntityQuery("select * from User order\nby name", true); + DefaultEntityQuery queryWithOrderAndLineBreakAndAlias = new TestEntityQuery( + "select * from User u order\nby name", true); + DefaultEntityQuery queryWithOrderAndMultipleLineBreakAndAlias = new TestEntityQuery( + "select * from User\nu\norder \n by name", true); assertThat(getEnhancer(queryWithOrderAndLineBreak).detectAlias()).isNull(); assertThat(getEnhancer(queryWithOrderAndLineBreakAndAlias).detectAlias()).isEqualTo("u"); @@ -537,7 +545,7 @@ class QueryEnhancerUnitTests { @ParameterizedTest // DATAJPA-1679 @MethodSource("findProjectionClauseWithDistinctSource") - void findProjectionClauseWithDistinct(IntrospectedQuery query, String expected) { + void findProjectionClauseWithDistinct(DefaultEntityQuery query, String expected) { SoftAssertions.assertSoftly(sofly -> sofly.assertThat(getEnhancer(query).getProjection()).isEqualTo(expected)); } @@ -545,10 +553,10 @@ class QueryEnhancerUnitTests { public static Stream findProjectionClauseWithDistinctSource() { return Stream.of( // - Arguments.of(new StringQuery("select * from x", true), "*"), // - Arguments.of(new StringQuery("select a, b, c from x", true), "a, b, c"), // - Arguments.of(new StringQuery("select distinct a, b, c from x", true), "a, b, c"), // - Arguments.of(new StringQuery("select DISTINCT a, b, c from x", true), "a, b, c") // + Arguments.of(new TestEntityQuery("select * from x", true), "*"), // + Arguments.of(new TestEntityQuery("select a, b, c from x", true), "a, b, c"), // + Arguments.of(new TestEntityQuery("select distinct a, b, c from x", true), "a, b, c"), // + Arguments.of(new TestEntityQuery("select DISTINCT a, b, c from x", true), "a, b, c") // ); } @@ -566,33 +574,17 @@ class QueryEnhancerUnitTests { // This is a required behavior the testcase in #findProjectionClauseWithSubselect tells why String queryString = "select * from (select x from y)"; - StringQuery query = new StringQuery(queryString, true); + DefaultEntityQuery query = new TestEntityQuery(queryString, true); assertThat(getEnhancer(query).getProjection()).isEqualTo("*"); } - @Disabled - @ParameterizedTest // DATAJPA-252 - @MethodSource("detectsJoinAliasesCorrectlySource") - void detectsJoinAliasesCorrectly(String queryString, List aliases) { - - StringQuery nativeQuery = new StringQuery(queryString, true); - StringQuery nonNativeQuery = new StringQuery(queryString, false); - - Set nativeJoinAliases = getEnhancer(nativeQuery).getJoinAliases(); - Set nonNativeJoinAliases = getEnhancer(nonNativeQuery).getJoinAliases(); - - assertThat(nonNativeJoinAliases).containsAll(nativeJoinAliases); - assertThat(nativeJoinAliases).hasSameSizeAs(aliases) // - .containsAll(aliases); - } - @Test // GH-2441 void correctFunctionAliasWithComplexNestedFunctions() { String queryString = "\nSELECT \nCAST(('{' || string_agg(distinct array_to_string(c.institutes_ids, ','), ',') || '}') AS bigint[]) as institutesIds\nFROM\ncity c"; - StringQuery nativeQuery = new StringQuery(queryString, true); + DefaultEntityQuery nativeQuery = new TestEntityQuery(queryString, true); JSqlParserQueryEnhancer queryEnhancer = (JSqlParserQueryEnhancer) getEnhancer(nativeQuery); assertThat(queryEnhancer.getSelectionAliases()).contains("institutesIds"); @@ -608,9 +600,10 @@ class QueryEnhancerUnitTests { + " city c\n" // + " ) dd"; - StringQuery nativeQuery = new StringQuery(queryString, true); + DefaultEntityQuery nativeQuery = new TestEntityQuery(queryString, true); QueryEnhancer queryEnhancer = getEnhancer(nativeQuery); - String result = queryEnhancer.applySorting(Sort.by(new Sort.Order(Sort.Direction.ASC, "institutesIds"))); + String result = queryEnhancer + .rewrite(getRewriteInformation(Sort.by(new Sort.Order(Sort.Direction.ASC, "institutesIds")))); assertThat(result).containsIgnoringCase("order by dd.institutesIds"); } @@ -625,23 +618,22 @@ class QueryEnhancerUnitTests { boolean constructorExpressionNotConsideringQueryType = QueryUtils.hasConstructorExpression(modifyingQuery); String countQueryForNotConsiderQueryType = QueryUtils.createCountQueryFor(modifyingQuery); - StringQuery modiQuery = new StringQuery(modifyingQuery, true); + DefaultEntityQuery modiQuery = new TestEntityQuery(modifyingQuery, true); assertThat(modiQuery.getAlias()).isEqualToIgnoringCase(aliasNotConsideringQueryType); assertThat(modiQuery.getProjection()).isEqualToIgnoringCase(projectionNotConsideringQueryType); assertThat(modiQuery.hasConstructorExpression()).isEqualTo(constructorExpressionNotConsideringQueryType); assertThat(countQueryForNotConsiderQueryType).isEqualToIgnoringCase(modifyingQuery); - assertThat(QueryEnhancerFactory.forQuery(modiQuery.getDeclaredQuery()).create(modiQuery.getDeclaredQuery()).createCountQueryFor()) - .isEqualToIgnoringCase(modifyingQuery); + assertThat(QueryEnhancer.create(modiQuery).createCountQueryFor(null)).isEqualToIgnoringCase(modifyingQuery); } @ParameterizedTest // GH-2593 @MethodSource("insertStatementIsProcessedSameAsDefaultSource") void insertStatementIsProcessedSameAsDefault(String insertQuery) { - StringQuery stringQuery = new StringQuery(insertQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery.getDeclaredQuery()); + DefaultEntityQuery stringQuery = new TestEntityQuery(insertQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancer.create(stringQuery); Sort sorting = Sort.by("day").descending(); @@ -657,11 +649,11 @@ class QueryEnhancerUnitTests { assertThat(stringQuery.hasConstructorExpression()).isFalse(); // access over enhancer - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(queryUtilsCountQuery); - assertThat(queryEnhancer.applySorting(sorting)).isEqualTo(insertQuery); // cant check with queryutils result since - // query utils appens order by which is not - // supported by sql standard. - assertThat(queryEnhancer.getJoinAliases()).isEqualTo(queryUtilsOuterJoinAlias); + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(queryUtilsCountQuery); + assertThat(queryEnhancer.rewrite(getRewriteInformation(sorting))).isEqualTo(insertQuery); // cant check with + // queryutils result since + // query utils appens order by which is not + // supported by sql standard. assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase(queryUtilsDetectAlias); assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase(queryUtilsProjection); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); @@ -689,15 +681,20 @@ class QueryEnhancerUnitTests { } private static void assertCountQuery(String originalQuery, String countQuery, boolean nativeQuery) { - assertCountQuery(new StringQuery(originalQuery, nativeQuery), countQuery); + assertCountQuery(new TestEntityQuery(originalQuery, nativeQuery), countQuery); } - private static void assertCountQuery(StringQuery originalQuery, String countQuery) { - assertThat(getEnhancer(originalQuery).createCountQueryFor()).isEqualToIgnoringCase(countQuery); + private static void assertCountQuery(DefaultEntityQuery originalQuery, String countQuery) { + assertThat(getEnhancer(originalQuery).createCountQueryFor(null)).isEqualToIgnoringCase(countQuery); } - private static QueryEnhancer getEnhancer(IntrospectedQuery query) { - return QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query.getDeclaredQuery()); + private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) { + return new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); + } + + private static QueryEnhancer getEnhancer(DeclaredQuery query) { + return QueryEnhancer.create(query); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java index 34d3ab239..d4fb9a761 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java @@ -53,7 +53,7 @@ class QueryParameterSetterFactoryUnitTests { @Test // DATAJPA-1058 void noExceptionWhenQueryDoesNotContainNamedParameters() { setterFactory.create(binding, - EntityQuery.introspectJpql("from Employee e", QueryEnhancerSelector.DEFAULT_SELECTOR)); + EntityQuery.create(DeclaredQuery.jpqlQuery("from Employee e"), QueryEnhancerSelector.DEFAULT_SELECTOR)); } @Test // DATAJPA-1058 @@ -63,7 +63,7 @@ class QueryParameterSetterFactoryUnitTests { assertThatExceptionOfType(IllegalStateException.class) // .isThrownBy(() -> setterFactory.create(binding, - EntityQuery.introspectJpql("from Employee e where e.name = :NamedParameter", + EntityQuery.create(DeclaredQuery.jpqlQuery("from Employee e where e.name = :NamedParameter"), QueryEnhancerSelector.DEFAULT_SELECTOR))) // .withMessageContaining("Java 8") // .withMessageContaining("@Param") // @@ -81,10 +81,9 @@ class QueryParameterSetterFactoryUnitTests { when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy( - () -> setterFactory.create(binding, - EntityQuery.introspectJpql("from Employee e where e.name = ?1", - QueryEnhancerSelector.DEFAULT_SELECTOR))) // + .isThrownBy(() -> setterFactory.create(binding, + EntityQuery.create(DeclaredQuery.jpqlQuery("from Employee e where e.name = ?1"), + QueryEnhancerSelector.DEFAULT_SELECTOR))) // .withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in 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 5887eab53..188166d3b 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 @@ -121,7 +121,8 @@ class SimpleJpaQueryUnitTests { extractor); when(em.createQuery("foo", Long.class)).thenReturn(typedQuery); - SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, CONFIG); + SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, method.getDeclaredQuery("select u from User u"), null, + CONFIG); assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {}))) .isEqualTo(typedQuery); @@ -135,7 +136,8 @@ class SimpleJpaQueryUnitTests { Method method = UserRepository.class.getMethod("findAllPaged", Pageable.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); - AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null, CONFIG); + AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, + queryMethod.getDeclaredQuery("select u from User u"), null, CONFIG); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -150,7 +152,7 @@ class SimpleJpaQueryUnitTests { Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); AbstractJpaQuery jpaQuery = JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, - queryMethod.getAnnotatedQuery(), null, CONFIG); + queryMethod.getRequiredDeclaredQuery(), null, CONFIG); assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); @@ -169,7 +171,7 @@ class SimpleJpaQueryUnitTests { Method method = SampleRepository.class.getMethod("findByLastnameNativeAnnotation", String.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); AbstractJpaQuery jpaQuery = JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, - queryMethod.getAnnotatedQuery(), null, CONFIG); + queryMethod.getRequiredDeclaredQuery(), null, CONFIG); assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); @@ -281,8 +283,9 @@ class SimpleJpaQueryUnitTests { Method method = SampleRepository.class.getMethod("findAllWithExpressionInCountQuery", Pageable.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); - AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", - "select count(u.id) from #{#entityName} u", CONFIG); + AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, + queryMethod.getDeclaredQuery("select u from User u"), + queryMethod.getDeclaredQuery("select count(u.id) from #{#entityName} u"), CONFIG); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -294,18 +297,18 @@ class SimpleJpaQueryUnitTests { return createJpaQuery(method, null); } - private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, - @Nullable String countQueryString) { + private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable DeclaredQuery query, + @Nullable DeclaredQuery countQzery) { - return JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, queryString, - countQueryString, CONFIG); + return JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, query, countQzery, + CONFIG); } - private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional countQueryString) { + private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional countQueryString) { JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); - return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(), - countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery())); + return createJpaQuery(queryMethod, queryMethod.getRequiredDeclaredQuery(), + countQueryString == null ? null : countQueryString.orElse(queryMethod.getDeclaredCountQuery())); } interface SampleRepository { 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/TemplatedQueryUnitTests.java similarity index 71% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TemplatedQueryUnitTests.java index a23554301..6581b628f 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/TemplatedQueryUnitTests.java @@ -33,7 +33,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.Part.Type; /** - * Unit tests for {@link ExpressionBasedStringQuery}. + * Unit tests for {@link TemplatedQuery}. * * @author Thomas Darimont * @author Oliver Gierke @@ -45,7 +45,7 @@ import org.springframework.data.repository.query.parser.Part.Type; */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class ExpressionBasedStringQueryUnitTests { +class TemplatedQueryUnitTests { private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); @@ -61,16 +61,14 @@ class ExpressionBasedStringQueryUnitTests { void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() { String source = "select u from #{#entityName} u where u.firstname like :firstname"; - StringQuery query = new ExpressionBasedStringQuery(source, metadata, - CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + EntityQuery query = jpqlEntityQuery(source); 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, - CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); + DefaultEntityQuery query = jpqlEntityQuery("select u from #{#entityName} u"); assertThat(query.getAlias()).isEqualTo("u"); assertThat(query.getQueryString()).isEqualTo("select u from User u"); } @@ -78,12 +76,11 @@ class ExpressionBasedStringQueryUnitTests { @Test // DATAJPA-1695 void shouldDetectBindParameterCountCorrectly() { - StringQuery query = new ExpressionBasedStringQuery( + EntityQuery query = jpqlEntityQuery( "select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(:#{#networkRequest.name})) OR :#{#networkRequest.name} IS NULL " + "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, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + + "AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})"); assertThat(query.getParameterBindings()).hasSize(8); } @@ -91,12 +88,11 @@ class ExpressionBasedStringQueryUnitTests { @Test // GH-2228 void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() { - StringQuery query = new ExpressionBasedStringQuery( + EntityQuery query = jpqlEntityQuery( "select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )" + "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, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + + "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})"); assertThat(query.getParameterBindings()).hasSize(8); } @@ -104,40 +100,28 @@ class ExpressionBasedStringQueryUnitTests { @Test void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { - StringQuery query = new ExpressionBasedStringQuery( + DefaultEntityQuery query = jpqlEntityQuery( "select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )" + "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, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + + "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})"); - assertThat(query.getDeclaredQuery().isNativeQuery()).isFalse(); - } - - @Test - void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { - - StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, - CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); - - assertThat(query.getDeclaredQuery().isNativeQuery()).isFalse(); + assertThat(query.isNative()).isFalse(); } @Test void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { - StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, - CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); + DefaultEntityQuery query = nativeEntityQuery("select u from User u"); - assertThat(query.getDeclaredQuery().isNativeQuery()).isTrue(); + assertThat(query.isNative()).isTrue(); } @Test // GH-3041 void namedExpressionsShouldCreateLikeBindings() { - StringQuery query = new ExpressionBasedStringQuery( - "select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, - CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + EntityQuery query = jpqlEntityQuery( + "select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%"); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo( @@ -160,9 +144,8 @@ class ExpressionBasedStringQueryUnitTests { @Test // GH-3041 void indexedExpressionsShouldCreateLikeBindings() { - StringQuery query = new ExpressionBasedStringQuery( - "select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, - CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + EntityQuery query = jpqlEntityQuery( + "select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%"); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()) @@ -185,8 +168,7 @@ class ExpressionBasedStringQueryUnitTests { @Test void doesTemplatingWhenEntityNameSpelIsPresent() { - StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from #{#entityName} u", - metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + EntityQuery query = jpqlEntityQuery("select #{#entityName + 'Hallo'} from #{#entityName} u"); assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u"); } @@ -194,8 +176,7 @@ class ExpressionBasedStringQueryUnitTests { @Test void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { - StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from User u", metadata, - CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + EntityQuery query = jpqlEntityQuery("select #{#entityName + 'Hallo'} from User u"); assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u"); } @@ -203,9 +184,16 @@ class ExpressionBasedStringQueryUnitTests { @Test void doesTemplatingWhenEntityNameSpelIsPresentForBindParameter() { - StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u where name = :#{#something}", - metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); + EntityQuery query = jpqlEntityQuery("select u from #{#entityName} u where name = :#{#something}"); assertThat(query.getQueryString()).isEqualTo("select u from User u where name = :__$synthetic$__1"); } + + private DefaultEntityQuery nativeEntityQuery(String source) { + return (DefaultEntityQuery) TemplatedQuery.create(DeclaredQuery.nativeQuery(source), metadata, CONFIG); + } + + private DefaultEntityQuery jpqlEntityQuery(String source) { + return (DefaultEntityQuery) TemplatedQuery.create(DeclaredQuery.jpqlQuery(source), metadata, CONFIG); + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java similarity index 53% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java index 6a8f3cce0..25c084890 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java @@ -16,23 +16,21 @@ package org.springframework.data.jpa.repository.query; /** - * @author Christoph Strobl + * Test-variant of {@link DefaultEntityQuery} with a simpler constructor. + * + * @author Mark Paluch */ -final class JpqlQuery implements DeclaredQuery { +class TestEntityQuery extends DefaultEntityQuery { - private final String jpql; + /** + * Creates a new {@link DefaultEntityQuery} from the given JPQL query. + * + * @param query must not be {@literal null} or empty. + */ + TestEntityQuery(String query, boolean isNative) { - JpqlQuery(String jpql) { - this.jpql = jpql; - } - - @Override - public boolean isNativeQuery() { - return false; - } - - @Override - public String getQueryString() { - return jpql; - } + super(PreprocessedQuery.parse(isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query)), + QueryEnhancerSelector.DEFAULT_SELECTOR + .select(isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query))); + } } 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 044e5268c..52b3e8a2c 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -308,17 +308,6 @@ public interface UserRepository extends JpaRepository { ---- ==== -[TIP] -==== -It is possible to disable usage of `JSqlParser` for parsing native queries although it is available on the classpath by setting `spring.data.jpa.query.native.parser=regex` via the `spring.properties` file or a system property. - -Valid values are (case-insensitive): - -* `auto` (default, automatic selection) -* `regex` (Use the builtin regex-based Query Enhancer) -* `jsqlparser` (Use JSqlParser) -==== - A similar approach also works with named native queries, by adding the `.count` suffix to a copy of your query. You probably need to register a result set mapping for your count query, though. Next to obtaining mapped results, native queries allow you to read the raw `Tuple` from the database by choosing a `Map` container as the method's return type. @@ -344,8 +333,120 @@ interface UserRepository extends JpaRepository { NOTE: String-based Tuple Queries are only supported by Hibernate. Eclipselink supports only Criteria-based Tuple Queries. -[[jpa.query-methods.at-query.projections]] +[[jpa.query-methods.query-introspection-rewriting]] +=== Query Introspection and Rewriting +Spring Data JPA provides a wide range of functionality that can be used to run various flavors of queries. +Specifically, given a declared query, Spring Data JPA can: + +* Introspect a query for its projection and run a tuple query for interface projections +* Use DTO projections if the query uses constructor expressions and rewrite the projection when the query declares the entity alias or just a multi-select of expressions +* Apply dynamic sorting +* Derive a `COUNT` query + +For this purpose, we ship with Query Parsers specific to HQL (Hibernate) and EQL (EclipseLink) dialects as these dialects are well-defined. +SQL on the other hand allows for quite some variance across dialects. +Because of this, there is no way Spring Data will ever be able to support all levels of query complexity. +We are not general purpose SQL parser library but one to increase developer productivity through making query execution simpler. +Our built-in SQL query enhancer supports only simple queries for introspection `COUNT` query derivation. +A more complex query will require either the usage of link:https://github.com/JSQLParser/JSqlParser[JSqlParser] or that you provide a `COUNT` query through `@Query(countQuery=…)`. +If JSqlParser is on the class path, Spring Data JPA will use it for native queries. + +For a fine-grained control over selection, you can configure javadoc:org.springframework.data.jpa.repository.query.QueryEnhancerSelector[] using `@EnableJpaRepositories`: + +.Spring Data JPA repositories using JavaConfig +==== +[source,java] +---- +@Configuration +@EnableJpaRepositories(queryEnhancerSelector = MyQueryEnhancerSelector.class) +class ApplicationConfig { + // … +} +---- +==== + +`QueryEnhancerSelector` is a strategy interface intended to select a javadoc:org.springframework.data.jpa.repository.query.QueryEnhancer[] based on a specific query. +You can also provide your own `QueryEnhancer` implementation if you want. + +[[jpa.query-methods.query-rewriter]] +=== Applying a QueryRewriter + +Sometimes, no matter how many features you try to apply, it seems impossible to get Spring Data JPA to apply every thing you'd like to a query before it is sent to the `EntityManager`. + +You have the ability to get your hands on the query, right before it's sent to the `EntityManager` and "rewrite" it. +That is, you can make any alterations at the last moment. + +.Declare a QueryRewriter using `@Query` +==== +[source,java] +---- +public interface MyRepository extends JpaRepository { + + @NativeQuery(value = "select original_user_alias.* from SD_USER original_user_alias", + queryRewriter = MyQueryRewriter.class) + List findByNativeQuery(String param); + + @Query(value = "select original_user_alias from User original_user_alias", + queryRewriter = MyQueryRewriter.class) + List findByNonNativeQuery(String param); +} +---- +==== + +This example shows both a native (pure SQL) rewriter as well as a JPQL query, both leveraging the same `QueryRewriter`. +In this scenario, Spring Data JPA will look for a bean registered in the application context of the corresponding type. + +You can write a query rewriter like this: + +.Example `QueryRewriter` +==== +[source,java] +---- +public class MyQueryRewriter implements QueryRewriter { + + @Override + public String rewrite(String query, Sort sort) { + return query.replaceAll("original_user_alias", "rewritten_user_alias"); + } +} +---- +==== + +You have to ensure your `QueryRewriter` is registered in the application context, whether it's by applying one of Spring Framework's +`@Component`-based annotations, or having it as part of a `@Bean` method inside an `@Configuration` class. + +Another option is to have the repository itself implement the interface. + +.Repository that provides the `QueryRewriter` +==== +[source,java] +---- +public interface MyRepository extends JpaRepository, QueryRewriter { + + @Query(value = "select original_user_alias.* from SD_USER original_user_alias", + nativeQuery = true, + queryRewriter = MyRepository.class) + List findByNativeQuery(String param); + + @Query(value = "select original_user_alias from User original_user_alias", + queryRewriter = MyRepository.class) + List findByNonNativeQuery(String param); + + @Override + default String rewrite(String query, Sort sort) { + return query.replaceAll("original_user_alias", "rewritten_user_alias"); + } +} +---- +==== + +Depending on what you're doing with your `QueryRewriter`, it may be advisable to have more than one, each registered with the application context. + +NOTE: In a CDI-based environment, Spring Data JPA will search the `BeanManager` for instances of your implementation of +`QueryRewriter`. + +[[jpa.query-methods.at-query.projections]] [[jpa.query-methods.sorting]] == Using Sort @@ -440,14 +541,14 @@ 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 Expressions +== Templated Queries and Expressions 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`. +Spring Data JPA supports a template 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: