From 3806dc3472f39c49fdd762928f1e2a4e67965395 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 27 Jun 2024 11:14:21 +0200 Subject: [PATCH 1/7] Prepare issue branch. --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 9c462b755b..e996e76b01 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-2989-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 0bdf2c8e7e..7b73fe5cb8 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 4.0.0-SNAPSHOT + 4.0.0-GH-2989-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-2989-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..a74120da66 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-2989-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index b6470bdc89..11516db6ff 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 4.0.0-SNAPSHOT + 4.0.0-GH-2989-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-2989-SNAPSHOT ../pom.xml From 47108479add1c3e76863f83b93dd27006dcb3761 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 27 Jun 2024 11:14:02 +0200 Subject: [PATCH 2/7] Introduce `QueryEnhancerSelector` to configure which `QueryEnhancerFactory` to use. Introduce QueryEnhancerSelector to EnableJpaRepositories. Also, split DeclaredQuery into two interfaces to resolve the inner cycle of query introspection while just a value object is being created. Introduce JpaQueryConfiguration to capture a multitude of configuration elements. --- .../repository/query/HqlParserBenchmarks.java | 4 +- .../JSqlParserQueryEnhancerBenchmarks.java | 2 +- .../config/EnableJpaRepositories.java | 8 + .../config/JpaRepositoryConfigExtension.java | 5 + .../query/AbstractStringBasedJpaQuery.java | 59 +++--- .../jpa/repository/query/DeclaredQuery.java | 87 ++------- .../query/DefaultDeclaredQuery.java | 68 +++++++ ...Query.java => EmptyIntrospectedQuery.java} | 20 ++- .../jpa/repository/query/EntityQuery.java | 107 +++++++++++ .../query/ExpressionBasedStringQuery.java | 33 ++-- .../repository/query/IntrospectedQuery.java | 53 ++++++ .../query/JpaQueryConfiguration.java | 57 ++++++ .../repository/query/JpaQueryEnhancer.java | 27 +-- .../jpa/repository/query/JpaQueryFactory.java | 68 ------- .../query/JpaQueryLookupStrategy.java | 126 +++++++------ .../data/jpa/repository/query/NamedQuery.java | 22 ++- .../jpa/repository/query/NativeJpaQuery.java | 10 +- .../query/ParameterBinderFactory.java | 16 +- .../jpa/repository/query/QueryEnhancer.java | 1 - .../query/QueryEnhancerFactories.java | 168 ++++++++++++++++++ .../query/QueryEnhancerFactory.java | 124 ++----------- .../query/QueryEnhancerSelector.java | 93 ++++++++++ .../query/QueryParameterSetterFactory.java | 31 +--- .../data/jpa/repository/query/QueryUtils.java | 6 +- .../jpa/repository/query/SimpleJpaQuery.java | 33 +--- .../jpa/repository/query/StringQuery.java | 77 ++++++-- .../support/JpaRepositoryFactory.java | 34 ++-- .../support/JpaRepositoryFactoryBean.java | 65 ++++++- ...ctStringBasedJpaQueryIntegrationTests.java | 7 +- .../AbstractStringBasedJpaQueryUnitTests.java | 13 +- .../query/DefaultQueryEnhancerUnitTests.java | 6 +- .../EqlParserQueryEnhancerUnitTests.java | 2 +- .../query/EqlQueryTransformerTests.java | 2 +- .../ExpressionBasedStringQueryUnitTests.java | 38 ++-- .../HqlParserQueryEnhancerUnitTests.java | 2 +- .../query/HqlQueryTransformerTests.java | 2 +- .../JSqlParserQueryEnhancerUnitTests.java | 26 +-- .../JpaQueryLookupStrategyUnitTests.java | 17 +- .../JpaQueryRewriteIntegrationTests.java | 25 ++- .../JpqlParserQueryEnhancerUnitTests.java | 2 +- .../query/JpqlQueryTransformerTests.java | 2 +- .../repository/query/NamedQueryUnitTests.java | 4 +- .../query/NativeJpaQueryUnitTests.java | 4 +- .../query/QueryEnhancerFactoryUnitTests.java | 82 +-------- .../query/QueryEnhancerTckTests.java | 11 +- .../query/QueryEnhancerUnitTests.java | 18 +- .../QueryParameterSetterFactoryUnitTests.java | 28 +-- .../query/SimpleJpaQueryUnitTests.java | 42 ++--- .../query/StringQueryUnitTests.java | 8 +- 49 files changed, 1060 insertions(+), 685 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{EmptyDeclaredQuery.java => EmptyIntrospectedQuery.java} (77%) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java 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 fd46a3f6c2..fb524d76bf 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 @@ -55,8 +55,8 @@ OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE "cost overrun" """; - query = DeclaredQuery.of(s, false); - enhancer = QueryEnhancerFactory.forQuery(query); + query = DeclaredQuery.ofJpql(s); + enhancer = QueryEnhancerFactory.forQuery(query).create(query); } } 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 845282e319..aeb1764c5c 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 @@ -56,7 +56,7 @@ public void doSetup() throws IOException { select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"""; - enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.of(s, true)); + enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.ofNative(s)); } } 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 3ff333ea7c..46ff8ed2d9 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 @@ -28,6 +28,7 @@ import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.config.BootstrapMode; import org.springframework.data.repository.config.DefaultRepositoryBaseClass; @@ -181,4 +182,11 @@ * @return a single character used for escaping. */ char escapeCharacter() default '\\'; + + /** + * Configures the {@link QueryEnhancerSelector} to select a query enhancer for query introspection and transformation. + * + * @return a {@link QueryEnhancerSelector} class providing a no-args constructor. + */ + Class queryEnhancerSelector() default QueryEnhancerSelector.DefaultQueryEnhancerSelector.class; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 32b8670802..7abdd4758e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -122,6 +122,11 @@ public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSo } builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(source).orElse('\\')); builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME); + + if (source instanceof AnnotationRepositoryConfigurationSource) { + builder.addPropertyValue("queryEnhancerSelector", + source.getAttribute("queryEnhancerSelector", Class.class).orElse(null)); + } } @Override 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 055483523a..be83fd2dc2 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 @@ -49,8 +49,8 @@ */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { - private final DeclaredQuery query; - private final Lazy countQuery; + private final StringQuery query; + private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; @@ -65,37 +65,32 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * @param em must not be {@literal null}. * @param queryString must not be {@literal null}. * @param countQueryString must not be {@literal null}. - * @param queryRewriter must not be {@literal null}. - * @param valueExpressionDelegate must not be {@literal null}. + * @param queryConfiguration must not be {@literal null}. */ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) { + @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) { super(method, em); Assert.hasText(queryString, "Query string must not be null or empty"); - Assert.notNull(valueExpressionDelegate, "ValueExpressionDelegate must not be null"); - Assert.notNull(queryRewriter, "QueryRewriter must not be null"); + Assert.notNull(queryConfiguration, "JpaQueryConfiguration must not be null"); - this.valueExpressionDelegate = valueExpressionDelegate; + this.valueExpressionDelegate = queryConfiguration.getValueExpressionDelegate(); this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters()); - this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), valueExpressionDelegate, - method.isNativeQuery()); + this.query = ExpressionBasedStringQuery.create(queryString, method, queryConfiguration); this.countQuery = Lazy.of(() -> { if (StringUtils.hasText(countQueryString)) { - - return new ExpressionBasedStringQuery(countQueryString, method.getEntityInformation(), valueExpressionDelegate, - method.isNativeQuery()); + return ExpressionBasedStringQuery.create(countQueryString, method, queryConfiguration); } - return query.deriveCountQuery(method.getCountQueryProjection()); + return this.query.deriveCountQuery(method.getCountQueryProjection()); }); this.countParameterBinder = Lazy.of(() -> this.createBinder(this.countQuery.get())); - this.queryRewriter = queryRewriter; + this.queryRewriter = queryConfiguration.getQueryRewriter(method); JpaParameters parameters = method.getParameters(); @@ -109,7 +104,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri } } - Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(), + Assert.isTrue(method.isNativeQuery() || !this.query.usesJdbcStyleParameters(), "JDBC style parameters (?) are not supported for JPA queries"); } @@ -136,7 +131,7 @@ protected ParameterBinder createBinder() { return createBinder(query); } - protected ParameterBinder createBinder(DeclaredQuery query) { + protected ParameterBinder createBinder(IntrospectedQuery query) { return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, valueExpressionDelegate, valueExpressionContextProvider); } @@ -160,14 +155,14 @@ protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) { /** * @return the query */ - public DeclaredQuery getQuery() { + public EntityQuery getQuery() { return query; } /** * @return the countQuery */ - public DeclaredQuery getCountQuery() { + public IntrospectedQuery getCountQuery() { return countQuery.get(); } @@ -208,8 +203,7 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla } String applySorting(CachableQuery cachableQuery) { - - return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()) + return cachableQuery.getDeclaredQuery().getQueryEnhancer() .rewrite(new DefaultQueryRewriteInformation(cachableQuery.getSort(), cachableQuery.getReturnedType())); } @@ -217,7 +211,7 @@ String applySorting(CachableQuery cachableQuery) { * Query Sort Rewriter interface. */ interface QuerySortRewriter { - String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType); + String getSorted(StringQuery query, Sort sort, ReturnedType returnedType); } /** @@ -227,9 +221,8 @@ enum SimpleQuerySortRewriter implements QuerySortRewriter { INSTANCE; - public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { - - return QueryEnhancerFactory.forQuery(query).rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); + public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { + return query.getQueryEnhancer().rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); } } @@ -237,7 +230,7 @@ static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter { private volatile @Nullable String cachedQueryString; - public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { + public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { if (sort.isSorted()) { throw new UnsupportedOperationException("NoOpQueryCache does not support sorting"); @@ -245,7 +238,7 @@ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedTyp String cachedQueryString = this.cachedQueryString; if (cachedQueryString == null) { - this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query) + this.cachedQueryString = cachedQueryString = query.getQueryEnhancer() .rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); } @@ -264,7 +257,7 @@ class CachingQuerySortRewriter implements QuerySortRewriter { private volatile @Nullable String cachedQueryString; @Override - public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { + public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { if (sort.isUnsorted()) { @@ -289,21 +282,21 @@ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedTyp */ static class CachableQuery { - private final DeclaredQuery declaredQuery; + private final StringQuery query; private final String queryString; private final Sort sort; private final ReturnedType returnedType; - CachableQuery(DeclaredQuery query, Sort sort, ReturnedType returnedType) { + CachableQuery(StringQuery query, Sort sort, ReturnedType returnedType) { - this.declaredQuery = query; + this.query = query; this.queryString = query.getQueryString(); this.sort = sort; this.returnedType = returnedType; } - DeclaredQuery getDeclaredQuery() { - return declaredQuery; + StringQuery getDeclaredQuery() { + return query; } Sort getSort() { 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 0e6f760ed3..ca32d1f46b 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 @@ -15,100 +15,45 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.List; - -import org.springframework.util.ObjectUtils; - -import org.jspecify.annotations.Nullable; - /** - * A wrapper for a String representation of a query offering information about the query. + * Interface defining the contract to represent a declared query. * * @author Jens Schauder * @author Diego Krupitza + * @author Mark Paluch * @since 2.0.3 */ -interface DeclaredQuery { +public interface DeclaredQuery { /** - * Creates a {@literal DeclaredQuery} from a query {@literal String}. + * Creates a DeclaredQuery for a JPQL query. * - * @param query might be {@literal null} or empty. - * @param nativeQuery is a given query is native or not - * @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument. + * @param query the JPQL query string. + * @return */ - static DeclaredQuery of(@Nullable String query, boolean nativeQuery) { - return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query, nativeQuery); + static DeclaredQuery ofJpql(String query) { + return new DefaultDeclaredQuery(query, false); } /** - * @return whether the underlying query has at least one named parameter. - */ - boolean hasNamedParameter(); - - /** - * Returns the query string. - */ - String getQueryString(); - - /** - * Returns the main alias used in the query. - * - * @return the alias - */ - @Nullable - String getAlias(); - - /** - * Returns whether the query is using a constructor expression. - * - * @since 1.10 - */ - boolean hasConstructorExpression(); - - /** - * 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(); - - /** - * Creates a new {@literal DeclaredQuery} 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. + * Creates a DeclaredQuery for a native query. * - * @param countQueryProjection an optional return type for the query. - * @return a new {@literal DeclaredQuery} instance. - */ - DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection); - - /** - * @return whether paging is implemented in the query itself, e.g. using SpEL expressions. - * @since 2.0.6 + * @param query the native query string. + * @return */ - default boolean usesPaging() { - return false; + static DeclaredQuery ofNative(String query) { + return new DefaultDeclaredQuery(query, true); } /** - * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or - * name. - * - * @return Whether the query uses JDBC style parameters. - * @since 2.0.6 + * Returns the query string. */ - boolean usesJdbcStyleParameters(); + String getQueryString(); /** * Return whether the query is a native query of not. * * @return true if native query otherwise false */ - default boolean isNativeQuery() { - return false; - } + boolean isNativeQuery(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java new file mode 100644 index 0000000000..a24512a994 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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; + +/** + * @author Mark Paluch + */ +class DefaultDeclaredQuery implements DeclaredQuery { + + private final String query; + private final boolean nativeQuery; + + DefaultDeclaredQuery(String query, boolean nativeQuery) { + this.query = query; + this.nativeQuery = nativeQuery; + } + + @Override + public String getQueryString() { + return query; + } + + @Override + public boolean isNativeQuery() { + return nativeQuery; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof DefaultDeclaredQuery that)) { + return false; + } + if (nativeQuery != that.nativeQuery) { + return false; + } + return ObjectUtils.nullSafeEquals(query, that.query); + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(query); + result = 31 * result + (nativeQuery ? 1 : 0); + return result; + } + + @Override + public String toString() { + return (isNativeQuery() ? "[native] " : "[JPQL] ") + getQueryString(); + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java similarity index 77% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java index 95693e8808..c51f0c4ca4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java @@ -18,20 +18,21 @@ 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 DeclaredQuery}. + * NULL-Object pattern implementation for {@link IntrospectedQuery}. * * @author Jens Schauder * @since 2.0.3 */ -class EmptyDeclaredQuery implements DeclaredQuery { +class EmptyIntrospectedQuery implements EntityQuery { /** * An implementation implementing the NULL-Object pattern for situations where there is no query. */ - static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery(); + static final EntityQuery EMPTY_QUERY = new EmptyIntrospectedQuery(); @Override public boolean hasNamedParameter() { @@ -43,11 +44,15 @@ public String getQueryString() { return ""; } - @Override public @Nullable String getAlias() { return null; } + @Override + public boolean isNativeQuery() { + return false; + } + @Override public boolean hasConstructorExpression() { return false; @@ -64,10 +69,15 @@ public List getParameterBindings() { } @Override - public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { + public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) { return EMPTY_QUERY; } + @Override + public String applySorting(Sort sort) { + return ""; + } + @Override public boolean usesJdbcStyleParameters() { return false; 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 new file mode 100644 index 0000000000..b959d3810e --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java @@ -0,0 +1,107 @@ +/* + * Copyright 2018-2024 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.data.domain.Sort; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * A wrapper for a String representation of a query offering information about the query. + * + * @author Jens Schauder + * @author Diego Krupitza + * @since 2.0.3 + */ +interface EntityQuery extends IntrospectedQuery { + + /** + * Creates a DeclaredQuery for a JPQL query. + * + * @param query the JPQL query string. + * @return + */ + static EntityQuery introspectJpql(String query, QueryEnhancerFactory queryEnhancer) { + return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY + : new StringQuery(query, false, queryEnhancer, parameterBindings -> {}); + } + + /** + * 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 -> {}); + } + + /** + * 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 -> {}); + } + + /** + * Returns whether the query is using a constructor expression. + * + * @since 1.10 + */ + boolean hasConstructorExpression(); + + /** + * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query. + */ + 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 + */ + default boolean usesPaging() { + return false; + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java index a414b52005..b6c93b5604 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java @@ -30,7 +30,7 @@ /** * Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression. *

- * Currently the following template variables are available: + * Currently, the following template variables are available: *

    *
  1. {@code #entityName} - the simple class name of the given entity
  2. *
      @@ -66,25 +66,13 @@ class ExpressionBasedStringQuery extends StringQuery { * @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 nativeQuery is a given query is native or not. + * @param selector must not be {@literal null}. */ - public ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, ValueExpressionParser parser, - boolean nativeQuery) { - super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query)); - } - - /** - * Creates an {@link ExpressionBasedStringQuery} from a given {@link DeclaredQuery}. - * - * @param query the original query. Must not be {@literal null}. - * @param metadata the {@link JpaEntityMetadata} for the given entity. Must not be {@literal null}. - * @param parser Parser for resolving SpEL expressions. Must not be {@literal null}. - * @param nativeQuery is a given query native or not - * @return A query supporting SpEL expressions. - */ - static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata metadata, - ValueExpressionParser parser, boolean nativeQuery) { - return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery); + ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, ValueExpressionParser parser, + boolean nativeQuery, QueryEnhancerSelector selector) { + super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query), + selector, parameterBindings -> {}); } /** @@ -131,4 +119,11 @@ private static String potentiallyQuoteExpressionsParameter(String query) { private static boolean containsExpression(String query) { 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/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java new file mode 100644 index 0000000000..427dbcc03b --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018-2024 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; + +/** + * A wrapper for a String representation of a query offering information about the query. + * + * @author Jens Schauder + * @author Diego Krupitza + * @since 2.0.3 + */ +interface IntrospectedQuery extends DeclaredQuery { + + /** + * @return whether the underlying query has at least one named 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(); + + /** + * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or + * name. + * + * @return Whether the query uses JDBC style parameters. + * @since 2.0.6 + */ + boolean usesJdbcStyleParameters(); + +} 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 new file mode 100644 index 0000000000..7bce8dc8f7 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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.data.jpa.repository.QueryRewriter; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +/** + * Configuration object holding configuration information for JPA queries within a repository. + * + * @author Mark Paluch + */ +public class JpaQueryConfiguration { + + private final QueryRewriterProvider queryRewriter; + private final QueryEnhancerSelector selector; + private final EscapeCharacter escapeCharacter; + private final ValueExpressionDelegate valueExpressionDelegate; + + public JpaQueryConfiguration(QueryRewriterProvider queryRewriter, QueryEnhancerSelector selector, + ValueExpressionDelegate valueExpressionDelegate, EscapeCharacter escapeCharacter) { + + this.queryRewriter = queryRewriter; + this.selector = selector; + this.escapeCharacter = escapeCharacter; + this.valueExpressionDelegate = valueExpressionDelegate; + } + + public QueryRewriter getQueryRewriter(JpaQueryMethod queryMethod) { + return queryRewriter.getQueryRewriter(queryMethod); + } + + public QueryEnhancerSelector getSelector() { + return selector; + } + + public EscapeCharacter getEscapeCharacter() { + return escapeCharacter; + } + + 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 1b57f4beb0..ff4b6efb7d 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 @@ -142,43 +142,34 @@ static void configureParser(String query, String grammar, Lexer lexer, Parser pa } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using JPQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using JPQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using JPQL. */ - public static JpaQueryEnhancer forJpql(DeclaredQuery query) { - - Assert.notNull(query, "DeclaredQuery must not be null!"); - - return JpqlQueryParser.parseQuery(query.getQueryString()); + public static JpaQueryEnhancer forJpql(String query) { + return JpqlQueryParser.parseQuery(query); } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using HQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using HQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using HQL. */ - public static JpaQueryEnhancer forHql(DeclaredQuery query) { - - Assert.notNull(query, "DeclaredQuery must not be null!"); - - return HqlQueryParser.parseQuery(query.getQueryString()); + public static JpaQueryEnhancer forHql(String query) { + return HqlQueryParser.parseQuery(query); } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using EQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using EQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using EQL. * @since 3.2 */ - public static JpaQueryEnhancer forEql(DeclaredQuery query) { - - Assert.notNull(query, "DeclaredQuery must not be null!"); - - return EqlQueryParser.parseQuery(query.getQueryString()); + public static JpaQueryEnhancer forEql(String query) { + return EqlQueryParser.parseQuery(query); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java deleted file mode 100644 index 384330af14..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2013-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 jakarta.persistence.EntityManager; - -import org.springframework.data.jpa.repository.QueryRewriter; - -import org.jspecify.annotations.Nullable; -import org.springframework.data.repository.query.QueryCreationException; -import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.data.repository.query.ValueExpressionDelegate; - -/** - * Factory to create the appropriate {@link RepositoryQuery} for a {@link JpaQueryMethod}. - * - * @author Thomas Darimont - * @author Mark Paluch - */ -enum JpaQueryFactory { - - INSTANCE; - - /** - * Creates a {@link RepositoryQuery} from the given {@link String} query. - */ - AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, QueryRewriter queryRewriter, - ValueExpressionDelegate valueExpressionDelegate) { - - if (method.isScrollQuery()) { - throw QueryCreationException.create(method, "Scroll queries are not supported using String-based queries"); - } - - return method.isNativeQuery() - ? new NativeJpaQuery(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate) - : new SimpleJpaQuery(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate); - } - - /** - * Creates a {@link StoredProcedureJpaQuery} from the given {@link JpaQueryMethod} query. - * - * @param method must not be {@literal null}. - * @param em must not be {@literal null}. - * @return - */ - public StoredProcedureJpaQuery fromProcedureAnnotation(JpaQueryMethod method, EntityManager em) { - - if (method.isScrollQuery()) { - throw QueryCreationException.create(method, "Scroll queries are not supported using stored procedures"); - } - - return new StoredProcedureJpaQuery(method, em); - } -} 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 db4c492eb7..cc2985fefc 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 @@ -24,10 +24,10 @@ import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethod; @@ -70,33 +70,31 @@ private abstract static class AbstractQueryLookupStrategy implements QueryLookup private final EntityManager em; private final JpaQueryMethodFactory queryMethodFactory; - private final QueryRewriterProvider queryRewriterProvider; + private final JpaQueryConfiguration configuration; /** * Creates a new {@link AbstractQueryLookupStrategy}. * * @param em must not be {@literal null}. * @param queryMethodFactory must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public AbstractQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - QueryRewriterProvider queryRewriterProvider) { - - Assert.notNull(em, "EntityManager must not be null"); - Assert.notNull(queryMethodFactory, "JpaQueryMethodFactory must not be null"); + JpaQueryConfiguration configuration) { this.em = em; this.queryMethodFactory = queryMethodFactory; - this.queryRewriterProvider = queryRewriterProvider; + this.configuration = configuration; } @Override public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { JpaQueryMethod queryMethod = queryMethodFactory.build(method, metadata, factory); - return resolveQuery(queryMethod, queryRewriterProvider.getQueryRewriter(queryMethod), em, namedQueries); + return resolveQuery(queryMethod, configuration, em, namedQueries); } - protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, + protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em, NamedQueries namedQueries); } @@ -109,20 +107,16 @@ protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewr */ private static class CreateQueryLookupStrategy extends AbstractQueryLookupStrategy { - private final EscapeCharacter escape; - public CreateQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - QueryRewriterProvider queryRewriterProvider, EscapeCharacter escape) { + JpaQueryConfiguration configuration) { - super(em, queryMethodFactory, queryRewriterProvider); - - this.escape = escape; + super(em, queryMethodFactory, configuration); } @Override - protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em, + protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em, NamedQueries namedQueries) { - return new PartTreeJpaQuery(method, em, escape); + return new PartTreeJpaQuery(method, em, configuration.getEscapeCharacter()); } } @@ -134,32 +128,27 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer * @author Thomas Darimont * @author Jens Schauder */ - private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy { - - private final ValueExpressionDelegate valueExpressionDelegate; + static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy { /** * Creates a new {@link DeclaredQueryLookupStrategy}. * * @param em must not be {@literal null}. * @param queryMethodFactory must not be {@literal null}. - * @param delegate must not be {@literal null}. - * @param queryRewriterProvider must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider) { + JpaQueryConfiguration configuration) { - super(em, queryMethodFactory, queryRewriterProvider); - - this.valueExpressionDelegate = delegate; + super(em, queryMethodFactory, configuration); } @Override - protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em, + protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em, NamedQueries namedQueries) { if (method.isProcedureQuery()) { - return JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em); + return createProcedureQuery(method, em); } if (StringUtils.hasText(method.getAnnotatedQuery())) { @@ -169,17 +158,17 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer "Query method %s is annotated with both, a query and a query name; Using the declared query", method)); } - return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getRequiredAnnotatedQuery(), - getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate); + return createStringQuery(method, em, method.getRequiredAnnotatedQuery(), + getCountQuery(method, namedQueries, em), configuration); } String name = method.getNamedQueryName(); if (namedQueries.hasQuery(name)) { - return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), - getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate); + return createStringQuery(method, em, namedQueries.getQuery(name), getCountQuery(method, namedQueries, em), + configuration); } - RepositoryQuery query = NamedQuery.lookupFrom(method, em); + RepositoryQuery query = NamedQuery.lookupFrom(method, em, configuration.getSelector()); return query != null // ? query // @@ -210,6 +199,44 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer return null; } + + /** + * Creates a {@link RepositoryQuery} from the given {@link String} query. + * + * @param method must not be {@literal null}. + * @param em must not be {@literal null}. + * @param queryString must not be {@literal null}. + * @param countQueryString must not be {@literal null}. + * @param configuration must not be {@literal null}. + * @return + */ + static AbstractJpaQuery createStringQuery(JpaQueryMethod method, EntityManager em, String queryString, + @Nullable String countQueryString, 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); + } + + /** + * Creates a {@link StoredProcedureJpaQuery} from the given {@link JpaQueryMethod} query. + * + * @param method must not be {@literal null}. + * @param em must not be {@literal null}. + * @return + */ + static StoredProcedureJpaQuery createProcedureQuery(JpaQueryMethod method, EntityManager em) { + + if (method.isScrollQuery()) { + throw QueryCreationException.create(method, "Scroll queries are not supported using stored procedures"); + } + + return new StoredProcedureJpaQuery(method, em); + } + } /** @@ -232,31 +259,29 @@ private static class CreateIfNotFoundQueryLookupStrategy extends AbstractQueryLo * @param queryMethodFactory must not be {@literal null}. * @param createStrategy must not be {@literal null}. * @param lookupStrategy must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public CreateIfNotFoundQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy, - QueryRewriterProvider queryRewriterProvider) { - - super(em, queryMethodFactory, queryRewriterProvider); + JpaQueryConfiguration configuration) { - Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null"); - Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null"); + super(em, queryMethodFactory, configuration); this.createStrategy = createStrategy; this.lookupStrategy = lookupStrategy; } @Override - protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em, + protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em, NamedQueries namedQueries) { - RepositoryQuery lookupQuery = lookupStrategy.resolveQuery(method, queryRewriter, em, namedQueries); + RepositoryQuery lookupQuery = lookupStrategy.resolveQuery(method, configuration, em, namedQueries); if (lookupQuery != NO_QUERY) { return lookupQuery; } - return createStrategy.resolveQuery(method, queryRewriter, em, namedQueries); + return createStrategy.resolveQuery(method, configuration, em, namedQueries); } } @@ -266,25 +291,20 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer * @param em must not be {@literal null}. * @param queryMethodFactory must not be {@literal null}. * @param key may be {@literal null}. - * @param delegate must not be {@literal null}. - * @param queryRewriterProvider must not be {@literal null}. - * @param escape must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - @Nullable Key key, ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider, - EscapeCharacter escape) { + @Nullable Key key, JpaQueryConfiguration configuration) { Assert.notNull(em, "EntityManager must not be null"); - Assert.notNull(delegate, "ValueExpressionDelegate must not be null"); + Assert.notNull(configuration, "JpaQueryConfiguration must not be null"); return switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) { - case CREATE -> new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape); - case USE_DECLARED_QUERY -> - new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider); + case CREATE -> new CreateQueryLookupStrategy(em, queryMethodFactory, configuration); + case USE_DECLARED_QUERY -> new DeclaredQueryLookupStrategy(em, queryMethodFactory, configuration); case CREATE_IF_NOT_FOUND -> new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory, - new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape), - new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider), - queryRewriterProvider); + new CreateQueryLookupStrategy(em, queryMethodFactory, configuration), + new DeclaredQueryLookupStrategy(em, queryMethodFactory, configuration), configuration); default -> throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key)); }; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index 7e3825aad8..2263f4d975 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 @@ -52,12 +52,12 @@ final class NamedQuery extends AbstractJpaQuery { private final String countQueryName; private final @Nullable String countProjection; private final boolean namedCountQueryIsPresent; - private final Lazy declaredQuery; + private final Lazy entityQuery; /** * Creates a new {@link NamedQuery}. */ - private NamedQuery(JpaQueryMethod method, EntityManager em) { + private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelector selector) { super(method, em); @@ -92,8 +92,12 @@ private NamedQuery(JpaQueryMethod method, EntityManager em) { String queryString = extractor.extractQueryString(query); - this.declaredQuery = Lazy - .of(() -> DeclaredQuery.of(queryString, method.isNativeQuery() || query.toString().contains("NativeQuery"))); + // TODO: What is queryString is null? + if (method.isNativeQuery() || (query != null && query.toString().contains("NativeQuery"))) { + this.entityQuery = Lazy.of(() -> EntityQuery.introspectNativeQuery(queryString, selector)); + } else { + this.entityQuery = Lazy.of(() -> EntityQuery.introspectJpql(queryString, selector)); + } } /** @@ -126,8 +130,10 @@ static boolean hasNamedQuery(EntityManager em, String queryName) { * * @param method must not be {@literal null}. * @param em must not be {@literal null}. + * @param selector must not be {@literal null}. */ - public static @Nullable RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em) { + public static @Nullable RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em, + QueryEnhancerSelector selector) { String queryName = method.getNamedQueryName(); @@ -145,7 +151,7 @@ static boolean hasNamedQuery(EntityManager em, String queryName) { method.isNativeQuery() ? "NativeQuery" : "Query")); } - RepositoryQuery query = new NamedQuery(method, em); + RepositoryQuery query = new NamedQuery(method, em, selector); if (LOG.isDebugEnabled()) { LOG.debug(String.format("Found named query '%s'", queryName)); } @@ -182,7 +188,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc } else { - String countQueryString = declaredQuery.get().deriveCountQuery(countProjection).getQueryString(); + String countQueryString = entityQuery.get().deriveCountQuery(countProjection).getQueryString(); cacheKey = countQueryString; countQuery = em.createQuery(countQueryString, Long.class); } @@ -212,7 +218,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc return type.isInterface() ? Tuple.class : null; } - return declaredQuery.get().hasConstructorExpression() // + return entityQuery.get().hasConstructorExpression() // ? null // : super.getTypeToRead(returnedType); } 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 ae240942d5..4c2fefe23f 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 @@ -26,7 +26,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.NativeQuery; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -43,7 +42,7 @@ * @author Mark Paluch * @author Greg Turnquist */ -final class NativeJpaQuery extends AbstractStringBasedJpaQuery { +class NativeJpaQuery extends AbstractStringBasedJpaQuery { private final @Nullable String sqlResultSetMapping; @@ -56,13 +55,12 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { * @param em must not be {@literal null}. * @param queryString must not be {@literal null} or empty. * @param countQueryString must not be {@literal null} or empty. - * @param rewriter the query rewriter to use. - * @param valueExpressionDelegate must not be {@literal null}. + * @param queryConfiguration must not be {@literal null}. */ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, - QueryRewriter rewriter, ValueExpressionDelegate valueExpressionDelegate) { + JpaQueryConfiguration queryConfiguration) { - super(method, em, queryString, countQueryString, rewriter, valueExpressionDelegate); + super(method, em, queryString, countQueryString, queryConfiguration); MergedAnnotations annotations = MergedAnnotations.from(method.getMethod()); MergedAnnotation annotation = annotations.get(NativeQuery.class); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index 384d5c16d7..8abf7d461d 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 @@ -84,7 +84,7 @@ static ParameterBinder createBinder(JpaParameters parameters, List getBindings(JpaParameters parameters) { @@ -124,26 +126,26 @@ static List getBindings(JpaParameters parameters) { private static Iterable createSetters(List parameterBindings, QueryParameterSetterFactory... factories) { - return createSetters(parameterBindings, EmptyDeclaredQuery.EMPTY_QUERY, factories); + return createSetters(parameterBindings, EmptyIntrospectedQuery.EMPTY_QUERY, factories); } private static Iterable createSetters(List parameterBindings, - DeclaredQuery declaredQuery, QueryParameterSetterFactory... strategies) { + IntrospectedQuery query, QueryParameterSetterFactory... strategies) { List setters = new ArrayList<>(parameterBindings.size()); for (ParameterBinding parameterBinding : parameterBindings) { - setters.add(createQueryParameterSetter(parameterBinding, strategies, declaredQuery)); + setters.add(createQueryParameterSetter(parameterBinding, strategies, query)); } return setters; } private static QueryParameterSetter createQueryParameterSetter(ParameterBinding binding, - QueryParameterSetterFactory[] strategies, DeclaredQuery declaredQuery) { + QueryParameterSetterFactory[] strategies, IntrospectedQuery query) { for (QueryParameterSetterFactory strategy : strategies) { - QueryParameterSetter setter = strategy.create(binding); + QueryParameterSetter setter = strategy.create(binding, query); if (setter != null) { return setter; 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 65304dcbba..ff9f44c44a 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 @@ -66,7 +66,6 @@ public interface QueryEnhancer { * * @return non-null {@link DeclaredQuery} that wraps the query. */ - @Deprecated(forRemoval = true) DeclaredQuery getQuery(); /** 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 new file mode 100644 index 0000000000..b88a6953f0 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java @@ -0,0 +1,168 @@ +/* + * Copyright 2024 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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.util.ClassUtils; + +/** + * Pre-defined QueryEnhancerFactories to be used for query enhancement. + * + * @author Mark Paluch + */ +public class QueryEnhancerFactories { + + private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class); + + static final boolean jSqlParserPresent = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser", + QueryEnhancerFactory.class.getClassLoader()); + + static { + + if (jSqlParserPresent) { + LOG.info("JSqlParser is in classpath; If applicable, JSqlParser will be used"); + } + + if (PersistenceProvider.ECLIPSELINK.isPresent()) { + LOG.info("EclipseLink is in classpath; If applicable, EQL parser will be used."); + } + + if (PersistenceProvider.HIBERNATE.isPresent()) { + LOG.info("Hibernate is in classpath; If applicable, HQL parser will be used."); + } + } + + enum BuiltinQueryEnhancerFactories implements QueryEnhancerFactory { + + FALLBACK { + @Override + public boolean supports(DeclaredQuery query) { + return true; + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + return new DefaultQueryEnhancer(query); + } + }, + + JSQLPARSER { + @Override + public boolean supports(DeclaredQuery query) { + return query.isNativeQuery(); + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + if (jSqlParserPresent) { + return new JSqlParserQueryEnhancer(query); + } + + throw new IllegalStateException("JSQLParser is not available on the class path"); + } + }, + + HQL { + @Override + public boolean supports(DeclaredQuery query) { + return !query.isNativeQuery(); + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + return JpaQueryEnhancer.forHql(query.getQueryString()); + } + }, + EQL { + @Override + public boolean supports(DeclaredQuery query) { + return !query.isNativeQuery(); + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + return JpaQueryEnhancer.forEql(query.getQueryString()); + } + }, + JPQL { + @Override + public boolean supports(DeclaredQuery query) { + return !query.isNativeQuery(); + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + return JpaQueryEnhancer.forJpql(query.getQueryString()); + } + } + } + + /** + * Returns the default fallback {@link QueryEnhancerFactory} using regex-based detection. This factory supports only + * simple SQL queries. + * + * @return fallback {@link QueryEnhancerFactory} using regex-based detection. + */ + public static QueryEnhancerFactory fallback() { + return BuiltinQueryEnhancerFactories.FALLBACK; + } + + /** + * Returns a {@link QueryEnhancerFactory} that uses JSqlParser + * if it is available from the class path. + * + * @return a {@link QueryEnhancerFactory} that uses JSqlParser. + * @throws IllegalStateException if JSQLParser is not on the class path. + */ + public static QueryEnhancerFactory jsqlparser() { + + if (!jSqlParserPresent) { + throw new IllegalStateException("JSQLParser is not available on the class path"); + } + + return BuiltinQueryEnhancerFactories.JSQLPARSER; + } + + /** + * Returns a {@link QueryEnhancerFactory} using HQL (Hibernate Query Language) parser. + * + * @return a {@link QueryEnhancerFactory} using HQL (Hibernate Query Language) parser. + */ + public static QueryEnhancerFactory hql() { + return BuiltinQueryEnhancerFactories.HQL; + } + + /** + * Returns a {@link QueryEnhancerFactory} using EQL (EclipseLink Query Language) parser. + * + * @return a {@link QueryEnhancerFactory} using EQL (EclipseLink Query Language) parser. + */ + public static QueryEnhancerFactory eql() { + return BuiltinQueryEnhancerFactories.EQL; + } + + /** + * Returns a {@link QueryEnhancerFactory} using JPQL (Jakarta Persistence Query Language) parser as per the JPA spec. + * + * @return a {@link QueryEnhancerFactory} using JPQL (Jakarta Persistence Query Language) parser as per the JPA spec. + */ + 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 5a2853cb1a..a3e7b5f06d 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 @@ -15,133 +15,41 @@ */ package org.springframework.data.jpa.repository.query; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.core.SpringProperties; -import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - /** - * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link DeclaredQuery}. + * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link IntrospectedQuery}. * * @author Diego Krupitza * @author Greg Turnquist * @author Mark Paluch * @author Christoph Strobl - * @since 2.7.0 + * @since 2.7 */ -public final class QueryEnhancerFactory { - - private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class); - private static final NativeQueryEnhancer NATIVE_QUERY_ENHANCER; - - static { - - NATIVE_QUERY_ENHANCER = NativeQueryEnhancer.select(); - - if (PersistenceProvider.ECLIPSELINK.isPresent()) { - LOG.info("EclipseLink is in classpath; If applicable, EQL parser will be used."); - } - - if (PersistenceProvider.HIBERNATE.isPresent()) { - LOG.info("Hibernate is in classpath; If applicable, HQL parser will be used."); - } - } - - private QueryEnhancerFactory() {} +public interface QueryEnhancerFactory { /** - * Creates a new {@link QueryEnhancer} for the given {@link DeclaredQuery}. + * Returns whether this QueryEnhancerFactory supports the given {@link DeclaredQuery}. * - * @param query must not be {@literal null}. - * @return an implementation of {@link QueryEnhancer} that suits the query the most + * @param query the query to be enhanced and introspected. + * @return {@code true} if this QueryEnhancer supports the given query; {@code false} otherwise. */ - public static QueryEnhancer forQuery(DeclaredQuery query) { - - if (query.isNativeQuery()) { - return getNativeQueryEnhancer(query); - } - - if (PersistenceProvider.HIBERNATE.isPresent()) { - return JpaQueryEnhancer.forHql(query); - } else if (PersistenceProvider.ECLIPSELINK.isPresent()) { - return JpaQueryEnhancer.forEql(query); - } else { - return JpaQueryEnhancer.forJpql(query); - } - } + boolean supports(DeclaredQuery query); /** - * Get the native query enhancer for the given {@link DeclaredQuery query} based on {@link #NATIVE_QUERY_ENHANCER}. + * Creates a new {@link QueryEnhancer} for the given query. * - * @param query the declared query. - * @return new instance of {@link QueryEnhancer}. + * @param query the query to be enhanced and introspected. + * @return */ - private static QueryEnhancer getNativeQueryEnhancer(DeclaredQuery query) { - - if (NATIVE_QUERY_ENHANCER.equals(NativeQueryEnhancer.JSQLPARSER)) { - return new JSqlParserQueryEnhancer(query); - } - - return new DefaultQueryEnhancer(query); - } + QueryEnhancer create(DeclaredQuery query); /** - * Possible choices for the {@link #NATIVE_PARSER_PROPERTY}. Resolve the parser through {@link #select()}. + * Creates a new {@link QueryEnhancerFactory} for the given {@link DeclaredQuery}. * - * @since 3.3.5 + * @param query must not be {@literal null}. + * @return an implementation of {@link QueryEnhancer} that suits the query the most */ - enum NativeQueryEnhancer { - - AUTO, REGEX, JSQLPARSER; - - static final String NATIVE_PARSER_PROPERTY = "spring.data.jpa.query.native.parser"; - - static final boolean JSQLPARSER_PRESENT = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser", null); - - /** - * @return the current selection considering classpath availability and user selection via - * {@link #NATIVE_PARSER_PROPERTY}. - */ - static NativeQueryEnhancer select() { - - NativeQueryEnhancer selected = resolve(); - - if (selected.equals(NativeQueryEnhancer.JSQLPARSER)) { - LOG.info("User choice: Using JSqlParser"); - return NativeQueryEnhancer.JSQLPARSER; - } - - if (selected.equals(NativeQueryEnhancer.REGEX)) { - LOG.info("Using Regex QueryEnhancer"); - return NativeQueryEnhancer.REGEX; - } - - if (!JSQLPARSER_PRESENT) { - return NativeQueryEnhancer.REGEX; - } - - LOG.info("JSqlParser is in classpath; If applicable, JSqlParser will be used."); - return NativeQueryEnhancer.JSQLPARSER; - } - - /** - * Resolve {@link NativeQueryEnhancer} from {@link SpringProperties}. - * - * @return the {@link NativeQueryEnhancer} constant. - */ - private static NativeQueryEnhancer resolve() { - - String name = SpringProperties.getProperty(NATIVE_PARSER_PROPERTY); - - if (StringUtils.hasText(name)) { - return ObjectUtils.caseInsensitiveValueOf(NativeQueryEnhancer.values(), name); - } - - return AUTO; - } + static QueryEnhancerFactory forQuery(DeclaredQuery query) { + return QueryEnhancerSelector.DEFAULT_SELECTOR.select(query); } } 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 new file mode 100644 index 0000000000..75bee83f1d --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 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.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. + * + * @author Mark Paluch + */ +public interface QueryEnhancerSelector { + + /** + * Default selector strategy. + */ + QueryEnhancerSelector DEFAULT_SELECTOR = new DefaultQueryEnhancerSelector(); + + /** + * Select a {@link QueryEnhancer} for a {@link DeclaredQuery query}. + * + * @param query + * @return + */ + QueryEnhancerFactory select(DeclaredQuery query); + + /** + * Default {@link QueryEnhancerSelector} implementation using class-path information to determine enhancer + * availability. Subclasses may provide a different configuration by using the protected constructor. + */ + class DefaultQueryEnhancerSelector implements QueryEnhancerSelector { + + protected static QueryEnhancerFactory DEFAULT_NATIVE; + protected static QueryEnhancerFactory DEFAULT_JPQL; + + static { + + DEFAULT_NATIVE = QueryEnhancerFactories.jSqlParserPresent ? QueryEnhancerFactories.jsqlparser() + : QueryEnhancerFactories.fallback(); + + if (PersistenceProvider.HIBERNATE.isPresent()) { + DEFAULT_JPQL = QueryEnhancerFactories.hql(); + } else if (PersistenceProvider.ECLIPSELINK.isPresent()) { + DEFAULT_JPQL = QueryEnhancerFactories.eql(); + } else { + DEFAULT_JPQL = QueryEnhancerFactories.jpql(); + } + } + + private final QueryEnhancerFactory nativeQuery; + private final QueryEnhancerFactory jpql; + + public DefaultQueryEnhancerSelector() { + this(DEFAULT_NATIVE, DEFAULT_JPQL); + } + + protected DefaultQueryEnhancerSelector(QueryEnhancerFactory nativeQuery, QueryEnhancerFactory jpql) { + this.nativeQuery = nativeQuery; + this.jpql = jpql; + } + + /** + * Returns the default JPQL {@link QueryEnhancerFactory} based on class path presence of Hibernate and EclipseLink. + * + * @return the default JPQL {@link QueryEnhancerFactory}. + */ + public static QueryEnhancerFactory jpql() { + return DEFAULT_JPQL; + } + + @Override + public QueryEnhancerFactory select(DeclaredQuery query) { + return jpql.supports(query) ? jpql : nativeQuery; + } + + } +} 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 3a6bb4c7e9..3a9d2af875 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 @@ -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); + abstract @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery); /** * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. @@ -116,8 +116,8 @@ private static QueryParameterSetter createSetter(Function parameters, String name) { @@ -180,7 +180,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) { return null; @@ -212,7 +212,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory { @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) { return null; @@ -248,7 +248,7 @@ private static class BasicQueryParameterSetterFactory extends QueryParameterSett } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { Assert.notNull(binding, "Binding must not be null"); @@ -294,22 +294,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding) { - - if (!binding.getOrigin().isMethodArgument()) { - return null; - } - - int parameterIndex = binding.getRequiredPosition() - 1; - - Assert.isTrue( // - parameterIndex < parameters.getNumberOfParameters(), // - () -> String.format( // - "At least %s parameter(s) provided but only %s parameter(s) present in query", // - binding.getRequiredPosition(), // - parameters.getNumberOfParameters() // - ) // - ); + public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { if (binding instanceof ParameterBinding.PartTreeParameterBinding ptb) { @@ -317,7 +302,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { return QueryParameterSetter.NOOP; } - return super.create(binding); + return super.create(binding, query); } return null; 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 41c572731e..1619dedb86 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,7 +445,7 @@ private static String toJpaDirection(Order order) { * * @param query must not be {@literal null}. * @return Might return {@literal null}. - * @deprecated use {@link DeclaredQuery#getAlias()} instead. + * @deprecated use {@link IntrospectedQuery#getAlias()} instead. */ @Deprecated public static @Nullable String detectAlias(String query) { @@ -554,7 +554,7 @@ public static Query applyAndBind(String queryString, Iterable entities, E * * @param originalQuery must not be {@literal null} or empty. * @return Guaranteed to be not {@literal null}. - * @deprecated use {@link DeclaredQuery#deriveCountQuery(String)} instead. + * @deprecated use {@link IntrospectedQuery#deriveCountQuery(String)} instead. */ @Deprecated public static String createCountQueryFor(String originalQuery) { @@ -568,7 +568,7 @@ public static String createCountQueryFor(String originalQuery) { * @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 DeclaredQuery#deriveCountQuery(String)} instead. + * @deprecated use {@link IntrospectedQuery#deriveCountQuery(String)} instead. */ @Deprecated public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) { 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 b43f555c12..b913061ad6 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 @@ -34,36 +34,21 @@ * @author Mark Paluch * @author Greg Turnquist */ -final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { - - /** - * Creates a new {@link SimpleJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}. - * - * @param method must not be {@literal null} - * @param em must not be {@literal null} - * @param countQueryString - * @param queryRewriter must not be {@literal null} - * @param valueExpressionDelegate must not be {@literal null} - */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString, - QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) { - this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryRewriter, valueExpressionDelegate); - } +class SimpleJpaQuery extends AbstractStringBasedJpaQuery { /** * Creates a new {@link SimpleJpaQuery} that encapsulates a simple query string. * - * @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 - * @param queryRewriter - * @param valueExpressionDelegate must not be {@literal null} + * @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 queryConfiguration must not be {@literal null}. */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, QueryRewriter queryRewriter, - ValueExpressionDelegate valueExpressionDelegate) { + public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, + JpaQueryConfiguration queryConfiguration) { - super(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate); + super(method, em, queryString, countQueryString, 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/StringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index b0b50cecb8..39af6fb1e3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -29,6 +29,7 @@ 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; @@ -61,13 +62,14 @@ * @author Greg Turnquist * @author Yuriy Tsarkov */ -class StringQuery implements DeclaredQuery { +class StringQuery implements EntityQuery { private final String query; private final List bindings; private final boolean containsPageableInSpel; private final boolean usesJdbcStyleParameters; private final boolean isNative; + private final QueryEnhancerFactory queryEnhancerFactory; private final QueryEnhancer queryEnhancer; private final boolean hasNamedParameters; @@ -77,7 +79,7 @@ class StringQuery implements DeclaredQuery { * @param query must not be {@literal null} or empty. */ public StringQuery(String query, boolean isNative) { - this(query, isNative, it -> {}); + this(query, isNative, QueryEnhancerSelector.DEFAULT_SELECTOR, it -> {}); } /** @@ -85,20 +87,21 @@ public StringQuery(String query, boolean isNative) { * * @param query must not be {@literal null} or empty. */ - private StringQuery(String query, boolean isNative, Consumer> parameterPostProcessor) { + StringQuery(String query, boolean isNative, QueryEnhancerFactory factory,Consumer> parameterPostProcessor) { Assert.hasText(query, "Query must not be null or empty"); this.isNative = isNative; this.bindings = new ArrayList<>(); this.containsPageableInSpel = query.contains("#pageable"); + this.queryEnhancerFactory = factory; Metadata queryMeta = new Metadata(); this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, this.bindings, queryMeta); this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; - this.queryEnhancer = QueryEnhancerFactory.forQuery(this); + this.queryEnhancer = factory.create(this); parameterPostProcessor.accept(this.bindings); @@ -113,6 +116,44 @@ private StringQuery(String query, boolean isNative, Consumer> parameterPostProcessor) { + + Assert.hasText(query, "Query must not be null or empty"); + + this.isNative = isNative; + this.bindings = new ArrayList<>(); + this.containsPageableInSpel = query.contains("#pageable"); + + Metadata queryMeta = new Metadata(); + this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, + this.bindings, queryMeta); + + this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; + this.queryEnhancerFactory = selector.select(this); + this.queryEnhancer = queryEnhancerFactory.create(this); + + parameterPostProcessor.accept(this.bindings); + + boolean hasNamedParameters = false; + for (ParameterBinding parameterBinding : getParameterBindings()) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { + hasNamedParameters = true; + break; + } + } + + this.hasNamedParameters = hasNamedParameters; + } + + QueryEnhancer getQueryEnhancer() { + return queryEnhancer; + } + /** * Returns whether we have found some like bindings. */ @@ -130,13 +171,13 @@ public List getParameterBindings() { } @Override - public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { + 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.isNative, derivedBindings -> { + this.isNative, queryEnhancerFactory, derivedBindings -> { // need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees // JPA @@ -158,6 +199,11 @@ public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { }); } + @Override + public String applySorting(Sort sort) { + return queryEnhancer.applySorting(sort); + } + @Override public boolean usesJdbcStyleParameters() { return usesJdbcStyleParameters; @@ -168,7 +214,6 @@ public String getQueryString() { return query; } - @Override public @Nullable String getAlias() { return queryEnhancer.detectAlias(); } @@ -404,16 +449,18 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que BindingIdentifier targetBinding = queryParameter; Function bindingFactory = switch (ParameterBindingType.of(typeSource)) { - case LIKE -> { + case LIKE -> { - Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); - yield (identifier) -> new 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. - default -> (identifier) -> new ParameterBinding(identifier, origin); - }; + Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); + yield (identifier) -> new 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. + default -> (identifier) -> new ParameterBinding(identifier, origin); + }; - if (origin.isExpression()) { + if (origin.isExpression()) { parameterBindings.register(bindingFactory.apply(queryParameter)); } else { targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory, parameterLabels); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 96d6277010..2e24577f8f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -35,17 +35,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.query.AbstractJpaQuery; -import org.springframework.data.jpa.repository.query.BeanFactoryQueryRewriterProvider; -import org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory; -import org.springframework.data.jpa.repository.query.EscapeCharacter; -import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy; -import org.springframework.data.jpa.repository.query.JpaQueryMethod; -import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory; -import org.springframework.data.jpa.repository.query.Procedure; -import org.springframework.data.jpa.repository.query.QueryRewriterProvider; +import org.springframework.data.jpa.repository.query.*; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; @@ -82,12 +73,12 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private final EntityManager entityManager; - private final QueryExtractor extractor; private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; private final CrudMethodMetadata crudMethodMetadata; private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private QueryEnhancerSelector queryEnhancerSelector = QueryEnhancerSelector.DEFAULT_SELECTOR; private JpaQueryMethodFactory queryMethodFactory; private QueryRewriterProvider queryRewriterProvider; @@ -101,7 +92,7 @@ public JpaRepositoryFactory(EntityManager entityManager) { Assert.notNull(entityManager, "EntityManager must not be null"); this.entityManager = entityManager; - this.extractor = PersistenceProvider.fromEntityManager(entityManager); + PersistenceProvider extractor = PersistenceProvider.fromEntityManager(entityManager); this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); this.entityPathResolver = SimpleEntityPathResolver.INSTANCE; this.queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor); @@ -179,6 +170,19 @@ public void setQueryMethodFactory(JpaQueryMethodFactory queryMethodFactory) { this.queryMethodFactory = queryMethodFactory; } + /** + * Configures the {@link QueryEnhancerSelector} to be used. Defaults to + * {@link QueryEnhancerSelector#DEFAULT_SELECTOR}. + * + * @param queryEnhancerSelector must not be {@literal null}. + */ + public void setQueryEnhancerSelector(QueryEnhancerSelector queryEnhancerSelector) { + + Assert.notNull(queryEnhancerSelector, "QueryEnhancerSelector must not be null"); + + this.queryEnhancerSelector = queryEnhancerSelector; + } + /** * Configures the {@link QueryRewriterProvider} to be used. Defaults to instantiate query rewriters through * {@link BeanUtils#instantiateClass(Class)}. @@ -243,8 +247,12 @@ protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoad @Override protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { + + JpaQueryConfiguration queryConfiguration = new JpaQueryConfiguration(queryRewriterProvider, queryEnhancerSelector, + new CachingValueExpressionDelegate(valueExpressionDelegate), escapeCharacter); + return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, - new CachingValueExpressionDelegate(valueExpressionDelegate), queryRewriterProvider, escapeCharacter)); + queryConfiguration)); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index e0a1b00e62..ebb24268d1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -18,12 +18,18 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import java.util.function.Function; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.SimpleEntityPathResolver; @@ -46,10 +52,12 @@ public class JpaRepositoryFactoryBean, S, ID> extends TransactionalRepositoryFactoryBeanSupport { + private @Nullable BeanFactory beanFactory; private @Nullable EntityManager entityManager; private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; private @Nullable JpaQueryMethodFactory queryMethodFactory; + private @Nullable Function queryEnhancerSelectorSource; /** * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. @@ -75,6 +83,12 @@ public void setMappingContext(MappingContext mappingContext) { super.setMappingContext(mappingContext); } + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + super.setBeanFactory(beanFactory); + } + /** * Configures the {@link EntityPathResolver} to be used. Will expect a canonical bean to be present but fallback to * {@link SimpleEntityPathResolver#INSTANCE} in case none is available. @@ -101,6 +115,43 @@ public void setQueryMethodFactory(@Nullable JpaQueryMethodFactory factory) { } } + /** + * Configures the {@link QueryEnhancerSelector} to be used. Defaults to + * {@link QueryEnhancerSelector#DEFAULT_SELECTOR}. + * + * @param queryEnhancerSelectorSource must not be {@literal null}. + */ + public void setQueryEnhancerSelectorSource(QueryEnhancerSelector queryEnhancerSelectorSource) { + this.queryEnhancerSelectorSource = bf -> queryEnhancerSelectorSource; + } + + /** + * Configures the {@link QueryEnhancerSelector} to be used. + * + * @param queryEnhancerSelectorType must not be {@literal null}. + */ + public void setQueryEnhancerSelector(Class queryEnhancerSelectorType) { + + this.queryEnhancerSelectorSource = bf -> { + + if (bf != null) { + + ObjectProvider beanProvider = bf.getBeanProvider(queryEnhancerSelectorType); + QueryEnhancerSelector selector = beanProvider.getIfAvailable(); + + if (selector != null) { + return selector; + } + + if (bf instanceof AutowireCapableBeanFactory acbf) { + return acbf.createBean(queryEnhancerSelectorType); + } + } + + return BeanUtils.instantiateClass(queryEnhancerSelectorType); + }; + } + @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { @@ -114,15 +165,19 @@ protected RepositoryFactorySupport doCreateRepositoryFactory() { */ protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { - JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); - jpaRepositoryFactory.setEntityPathResolver(entityPathResolver); - jpaRepositoryFactory.setEscapeCharacter(escapeCharacter); + JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager); + factory.setEntityPathResolver(entityPathResolver); + factory.setEscapeCharacter(escapeCharacter); if (queryMethodFactory != null) { - jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory); + factory.setQueryMethodFactory(queryMethodFactory); + } + + if (queryEnhancerSelectorSource != null) { + factory.setQueryEnhancerSelector(queryEnhancerSelectorSource.apply(beanFactory)); } - return jpaRepositoryFactory; + return factory; } @Override 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 6590db4022..204471b6d9 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 @@ -34,7 +34,6 @@ import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; @@ -53,6 +52,9 @@ @ContextConfiguration("classpath:infrastructure.xml") class AbstractStringBasedJpaQueryIntegrationTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + @PersistenceContext EntityManager em; @Autowired BeanFactory beanFactory; @@ -66,8 +68,7 @@ void createsNormalQueryForJpaManagedReturnTypes() throws Exception { when(mock.getMetamodel()).thenReturn(em.getMetamodel()); JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class); - AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, null, QueryRewriter.IdentityQueryRewriter.INSTANCE, - ValueExpressionDelegate.create()); + AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, method.getAnnotatedQuery(), null, CONFIG); jpaQuery.createJpaQuery(method.getAnnotatedQuery(), Sort.unsorted(), null, method.getResultProcessor().getReturnedType()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java index 44d061094f..adc489cc98 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 @@ -36,7 +36,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; @@ -56,6 +55,9 @@ */ class AbstractStringBasedJpaQueryUnitTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + @Test // GH-3310 void shouldNotAttemptToAppendSortIfNoSortArgumentPresent() { @@ -118,8 +120,8 @@ static InvocationCapturingStringQueryStub forMethod(Class repository, String Query query = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class); - return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery()); - + return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery(), + CONFIG); } static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQuery { @@ -128,7 +130,7 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu private final MultiValueMap capturedArguments = new LinkedMultiValueMap<>(3); InvocationCapturingStringQueryStub(Method targetMethod, JpaQueryMethod queryMethod, String queryString, - @Nullable String countQueryString) { + @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) { super(queryMethod, new Supplier() { @Override @@ -142,8 +144,7 @@ public EntityManager get() { return em; } - }.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class), - ValueExpressionDelegate.create()); + }.get(), queryString, countQueryString, queryConfiguration); this.targetMethod = targetMethod; } 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 6b9c4e2478..e0488df118 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 @@ -31,8 +31,8 @@ class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override - QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) { - return new DefaultQueryEnhancer(declaredQuery); + QueryEnhancer createQueryEnhancer(DeclaredQuery query) { + return new DefaultQueryEnhancer(query); } @Override @@ -43,7 +43,7 @@ void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {} @Test // GH-3546 void shouldApplySorting() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative("SELECT e FROM Employee e")); String sql = enhancer.applySorting(Sort.by("foo", "bar")); 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 8895fc4c19..5303378b84 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 @@ -32,7 +32,7 @@ QueryEnhancer createQueryEnhancer(DeclaredQuery query) { assumeThat(query.isNativeQuery()).isFalse(); - return JpaQueryEnhancer.forEql(query); + return JpaQueryEnhancer.forEql(query.getQueryString()); } @Override 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 782c460a24..61436aae55 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 @@ -827,6 +827,6 @@ private String projection(String query) { } private QueryEnhancer newParser(String query) { - return JpaQueryEnhancer.forEql(DeclaredQuery.of(query, false)); + return JpaQueryEnhancer.forEql(query); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java index 2b81871822..8e8528a4bd 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java @@ -28,8 +28,8 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.Part.Type; /** @@ -47,7 +47,9 @@ @MockitoSettings(strictness = Strictness.LENIENT) class ExpressionBasedStringQueryUnitTests { - private static final ValueExpressionParser PARSER = ValueExpressionParser.create(); + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + @Mock JpaEntityMetadata metadata; @BeforeEach @@ -59,14 +61,16 @@ void setUp() { void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() { String source = "select u from #{#entityName} u where u.firstname like :firstname"; - StringQuery query = new ExpressionBasedStringQuery(source, metadata, PARSER, false); + StringQuery query = new ExpressionBasedStringQuery(source, metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); 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, PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); assertThat(query.getAlias()).isEqualTo("u"); assertThat(query.getQueryString()).isEqualTo("select u from User u"); } @@ -79,7 +83,7 @@ void shouldDetectBindParameterCountCorrectly() { + "AND (LOWER(n.server) LIKE LOWER(:#{#networkRequest.server})) OR :#{#networkRequest.server} IS NULL " + "AND (n.createdAt >= :#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=:#{#networkRequest.createdTime.endDateTime}) " + "AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})", - metadata, PARSER, false); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getParameterBindings()).hasSize(8); } @@ -92,7 +96,7 @@ void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() { + "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)" + "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})" + "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})", - metadata, PARSER, false); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getParameterBindings()).hasSize(8); } @@ -105,7 +109,7 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { + "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)" + "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})" + "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})", - metadata, PARSER, true); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.isNativeQuery()).isFalse(); } @@ -113,7 +117,8 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { @Test void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { - StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); assertThat(query.isNativeQuery()).isFalse(); } @@ -121,7 +126,8 @@ void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { @Test void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { - StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); assertThat(query.isNativeQuery()).isTrue(); } @@ -130,8 +136,8 @@ void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { void namedExpressionsShouldCreateLikeBindings() { StringQuery query = new ExpressionBasedStringQuery( - "select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, PARSER, - false); + "select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo( @@ -155,8 +161,8 @@ void namedExpressionsShouldCreateLikeBindings() { void indexedExpressionsShouldCreateLikeBindings() { StringQuery query = new ExpressionBasedStringQuery( - "select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, PARSER, - false); + "select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()) @@ -180,7 +186,7 @@ void indexedExpressionsShouldCreateLikeBindings() { void doesTemplatingWhenEntityNameSpelIsPresent() { StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from #{#entityName} u", - metadata, PARSER, false); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u"); } @@ -189,7 +195,7 @@ void doesTemplatingWhenEntityNameSpelIsPresent() { void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from User u", metadata, - PARSER, false); + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u"); } @@ -198,7 +204,7 @@ void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { void doesTemplatingWhenEntityNameSpelIsPresentForBindParameter() { StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u where name = :#{#something}", - metadata, PARSER, false); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getQueryString()).isEqualTo("select u from User u where name = :__$synthetic$__1"); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java index ef7b269115..916db5e06a 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 @@ -32,7 +32,7 @@ QueryEnhancer createQueryEnhancer(DeclaredQuery query) { assumeThat(query.isNativeQuery()).isFalse(); - return JpaQueryEnhancer.forHql(query); + return JpaQueryEnhancer.forHql(query.getQueryString()); } @Override 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 1098f6a623..d9634ea91c 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 @@ -1196,6 +1196,6 @@ private String projection(String query) { } private QueryEnhancer newParser(String query) { - return JpaQueryEnhancer.forHql(DeclaredQuery.of(query, false)); + return JpaQueryEnhancer.forHql(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 dee9d10d66..a3977b8a64 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 @@ -37,14 +37,14 @@ public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override - QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) { - return new JSqlParserQueryEnhancer(declaredQuery); + QueryEnhancer createQueryEnhancer(DeclaredQuery query) { + return new JSqlParserQueryEnhancer(query); } @Test // GH-3546 void shouldApplySorting() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofJpql("SELECT e FROM Employee e")); String sql = enhancer.applySorting(Sort.by("foo", "bar")); @@ -54,13 +54,13 @@ void shouldApplySorting() { @Test // GH-3707 void countQueriesShouldConsiderPrimaryTableAlias() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of(""" + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(""" SELECT DISTINCT a.*, b.b1 FROM TableA a JOIN TableB b ON a.b = b.b LEFT JOIN TableC c ON b.c = c.c ORDER BY b.b1, a.a1, a.a2 - """, true)); + """)); String sql = enhancer.createCountQueryFor(); @@ -83,7 +83,7 @@ void setOperationListWorks() { + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); @@ -106,7 +106,7 @@ void complexSetOperationListWorks() { + "union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); @@ -133,7 +133,7 @@ void deeplyNestedcomplexSetOperationListWorks() { + "\t;"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("CustomerID"); @@ -153,7 +153,7 @@ void valuesStatementsWorks() { String setQuery = "VALUES (1, 2, 'test')"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isNullOrEmpty(); @@ -174,7 +174,7 @@ void withStatementsWorks() { + "select day, value from sample_data as a"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); @@ -197,7 +197,7 @@ void multipleWithStatementsWorks() { + "select day, value from sample_data as a"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); @@ -217,7 +217,7 @@ void multipleWithStatementsWorks() { void truncateStatementShouldWork() { StringQuery stringQuery = new StringQuery("TRUNCATE TABLE foo", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNull(); assertThat(stringQuery.getProjection()).isEmpty(); @@ -235,7 +235,7 @@ void truncateStatementShouldWork() { void mergeStatementWorksWithJSqlParser(String query, String alias) { StringQuery stringQuery = new StringQuery(query, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); assertThat(QueryUtils.detectAlias(query)).isNull(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index 861272154b..e68faf4092 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -34,7 +34,6 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.beans.factory.BeanFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -64,7 +63,8 @@ @MockitoSettings(strictness = Strictness.LENIENT) class JpaQueryLookupStrategyUnitTests { - private static final ValueExpressionDelegate VALUE_EXPRESSION_DELEGATE = ValueExpressionDelegate.create(); + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); @Mock EntityManager em; @Mock EntityManagerFactory emf; @@ -72,7 +72,6 @@ class JpaQueryLookupStrategyUnitTests { @Mock NamedQueries namedQueries; @Mock Metamodel metamodel; @Mock ProjectionFactory projectionFactory; - @Mock BeanFactory beanFactory; private JpaQueryMethodFactory queryMethodFactory; @@ -90,7 +89,7 @@ void setUp() { void invalidAnnotatedQueryCausesException() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); Method method = UserRepository.class.getMethod("findByFoo", String.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -102,7 +101,7 @@ void invalidAnnotatedQueryCausesException() throws Exception { void considersNamedCountQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); when(namedQueries.hasQuery("foo.count")).thenReturn(true); when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo"); @@ -124,7 +123,7 @@ void considersNamedCountQuery() throws Exception { void considersNamedCountOnStringQueryQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); when(namedQueries.hasQuery("foo.count")).thenReturn(true); when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo"); @@ -143,7 +142,7 @@ void considersNamedCountOnStringQueryQuery() throws Exception { void prefersDeclaredQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); Method method = UserRepository.class.getMethod("annotatedQueryWithQueryAndQueryName"); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -156,7 +155,7 @@ void prefersDeclaredQuery() throws Exception { void namedQueryWithSortShouldThrowIllegalStateException() throws NoSuchMethodException { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); Method method = UserRepository.class.getMethod("customNamedQuery", String.class, Sort.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -181,7 +180,7 @@ void noQueryShouldNotBeInvoked() { void customQueryWithQuestionMarksShouldWork() throws NoSuchMethodException { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); Method namedMethod = UserRepository.class.getMethod("customQueryWithQuestionMarksAndNamedParam", String.class); RepositoryMetadata namedMetadata = new DefaultRepositoryMetadata(UserRepository.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java index 9738c7843a..fa335ecee6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java @@ -15,8 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.Assertions.*; import java.util.HashMap; import java.util.List; @@ -25,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -40,8 +40,11 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.util.ReflectionTestUtils; /** * Unit tests for repository with {@link Query} and {@link QueryRewrite}. @@ -54,6 +57,7 @@ class JpaQueryRewriteIntegrationTests { @Autowired private UserRepositoryWithRewriter repository; + @Autowired private JpaRepositoryFactoryBean factoryBean; // Results static final String ORIGINAL_QUERY = "original query"; @@ -66,6 +70,14 @@ void setUp() { results.clear(); } + @Test + void shouldConfigureQueryEnhancerSelector() { + + JpaRepositoryFactory factory = (JpaRepositoryFactory) ReflectionTestUtils.getField(factoryBean, "factory"); + + assertThat(factory).extracting("queryEnhancerSelector").isInstanceOf(MyQueryEnhancerSelector.class); + } + @Test void nativeQueryShouldHandleRewrites() { @@ -222,7 +234,8 @@ private static String replaceAlias(String query, Sort sort) { @ImportResource("classpath:infrastructure.xml") @EnableJpaRepositories(considerNestedRepositories = true, basePackageClasses = UserRepositoryWithRewriter.class, // includeFilters = @ComponentScan.Filter(value = { UserRepositoryWithRewriter.class }, - type = FilterType.ASSIGNABLE_TYPE)) + type = FilterType.ASSIGNABLE_TYPE), + queryEnhancerSelector = MyQueryEnhancerSelector.class) static class JpaRepositoryConfig { @Bean @@ -231,4 +244,10 @@ QueryRewriter queryRewriter() { } } + + static class MyQueryEnhancerSelector extends QueryEnhancerSelector.DefaultQueryEnhancerSelector { + public MyQueryEnhancerSelector() { + super(QueryEnhancerFactories.fallback(), DefaultQueryEnhancerSelector.jpql()); + } + } } 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 8b6385e65d..32f9e965a9 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 @@ -32,7 +32,7 @@ QueryEnhancer createQueryEnhancer(DeclaredQuery query) { assumeThat(query.isNativeQuery()).isFalse(); - return JpaQueryEnhancer.forJpql(query); + return JpaQueryEnhancer.forJpql(query.getQueryString()); } @Override 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 acc6617811..1a38f729e2 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 @@ -832,6 +832,6 @@ private String projection(String query) { } private QueryEnhancer newParser(String query) { - return JpaQueryEnhancer.forJpql(DeclaredQuery.of(query, false)); + return JpaQueryEnhancer.forJpql(query); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java index dadfd1083d..77a8496122 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java @@ -88,7 +88,7 @@ void rejectsPersistenceProviderIfIncapableOfExtractingQueriesAndPagebleBeingUsed JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, projectionFactory, extractor); when(em.createNamedQuery(queryMethod.getNamedCountQueryName())).thenThrow(new IllegalArgumentException()); - assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em)); + assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em, QueryEnhancerSelector.DEFAULT_SELECTOR)); } @Test // DATAJPA-142 @@ -100,7 +100,7 @@ void doesNotRejectPersistenceProviderIfNamedCountQueryIsAvailable() { TypedQuery countQuery = mock(TypedQuery.class); when(em.createNamedQuery(eq(queryMethod.getNamedCountQueryName()), eq(Long.class))).thenReturn(countQuery); - NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em); + NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em, QueryEnhancerSelector.DEFAULT_SELECTOR); query.doCreateCountQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[1])); verify(em, times(1)).createNamedQuery(queryMethod.getNamedCountQueryName(), Long.class); 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 cf9dab51fb..fa44d2ca11 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 @@ -34,7 +34,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; @@ -75,7 +74,8 @@ void shouldApplySorting() { Query annotation = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class); NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, annotation.value(), annotation.countQuery(), - QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); + new JpaQueryConfiguration(QueryRewriterProvider.simple(), QueryEnhancerSelector.DEFAULT_SELECTOR, + ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT)); String sql = query.getSortedQueryString(Sort.by("foo", "bar"), queryMethod.getResultProcessor().getReturnedType()); assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc"); 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 f95e9007b1..7456e047c2 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 @@ -17,16 +17,7 @@ import static org.assertj.core.api.Assertions.*; -import java.util.stream.Stream; - -import org.jspecify.annotations.Nullable; 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.data.jpa.repository.query.QueryEnhancerFactory.NativeQueryEnhancer; -import org.springframework.data.jpa.util.ClassPathExclusions; /** * Unit tests for {@link QueryEnhancerFactory}. @@ -43,7 +34,7 @@ void createsParsingImplementationForNonNativeQuery() { StringQuery query = new StringQuery("select new com.example.User(u.firstname) from User u", false); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); assertThat(queryEnhancer) // .isInstanceOf(JpaQueryEnhancer.class); @@ -58,79 +49,10 @@ void createsJSqlImplementationForNativeQuery() { StringQuery query = new StringQuery("select * from User", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); assertThat(queryEnhancer) // .isInstanceOf(JSqlParserQueryEnhancer.class); } - @ParameterizedTest // GH-2989 - @MethodSource("nativeEnhancerSelectionArgs") - void createsNativeImplementationAccordingToUserChoice(@Nullable String selection, NativeQueryEnhancer enhancer) { - - assertThat(NativeQueryEnhancer.JSQLPARSER_PRESENT).isTrue(); - - withSystemProperty(NativeQueryEnhancer.NATIVE_PARSER_PROPERTY, selection, () -> { - assertThat(NativeQueryEnhancer.select()).isEqualTo(enhancer); - }); - } - - static Stream nativeEnhancerSelectionArgs() { - return Stream.of(Arguments.of(null, NativeQueryEnhancer.JSQLPARSER), // - Arguments.of("", NativeQueryEnhancer.JSQLPARSER), // - Arguments.of("auto", NativeQueryEnhancer.JSQLPARSER), // - Arguments.of("regex", NativeQueryEnhancer.REGEX), // - Arguments.of("jsqlparser", NativeQueryEnhancer.JSQLPARSER)); - } - - @ParameterizedTest // GH-2989 - @MethodSource("nativeEnhancerExclusionSelectionArgs") - @ClassPathExclusions(packages = { "net.sf.jsqlparser.parser" }) - void createsNativeImplementationAccordingWithoutJsqlParserToUserChoice(@Nullable String selection, - NativeQueryEnhancer enhancer) { - - assertThat(NativeQueryEnhancer.JSQLPARSER_PRESENT).isFalse(); - - withSystemProperty(NativeQueryEnhancer.NATIVE_PARSER_PROPERTY, selection, () -> { - assertThat(NativeQueryEnhancer.select()).isEqualTo(enhancer); - }); - } - - static Stream nativeEnhancerExclusionSelectionArgs() { - return Stream.of(Arguments.of(null, NativeQueryEnhancer.REGEX), // - Arguments.of("", NativeQueryEnhancer.REGEX), // - Arguments.of("auto", NativeQueryEnhancer.REGEX), // - Arguments.of("regex", NativeQueryEnhancer.REGEX), // - Arguments.of("jsqlparser", NativeQueryEnhancer.JSQLPARSER)); - } - - @Test // GH-2989 - @ClassPathExclusions(packages = { "net.sf.jsqlparser.parser" }) - void selectedDefaultImplementationIfJsqlNotAvailable() { - - assertThat(NativeQueryEnhancer.JSQLPARSER_PRESENT).isFalse(); - assertThat(NativeQueryEnhancer.select()).isEqualTo(NativeQueryEnhancer.REGEX); - } - - void withSystemProperty(String property, @Nullable String value, Runnable exeution) { - - String currentValue = System.getProperty(property); - if (value != null) { - System.setProperty(property, value); - } else { - System.clearProperty(property); - } - try { - exeution.run(); - } finally { - if (currentValue != null) { - System.setProperty(property, currentValue); - } else { - System.clearProperty(property); - } - } - - } - - } 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 077d469177..4b4bb8dfe0 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 @@ -35,8 +35,7 @@ abstract class QueryEnhancerTckTests { @MethodSource("nativeCountQueries") // GH-2773 void shouldDeriveNativeCountQuery(String query, String expected) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, true); - QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(query)); String countQueryFor = enhancer.createCountQueryFor(); // lenient cleanup to allow for rendering variance @@ -120,8 +119,7 @@ static Stream nativeCountQueries() { @MethodSource("jpqlCountQueries") void shouldDeriveJpqlCountQuery(String query, String expected) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, false); - QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofJpql(query)); String countQueryFor = enhancer.createCountQueryFor(null); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -180,8 +178,7 @@ static Stream jpqlCountQueries() { @MethodSource("nativeQueriesWithVariables") void shouldDeriveNativeCountQueryWithVariable(String query, String expected) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, true); - QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(query)); String countQueryFor = enhancer.createCountQueryFor(); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -211,6 +208,6 @@ void findProjectionClauseWithIncludedFrom() { assertThat(createQueryEnhancer(query).getProjection()).isEqualTo("x, frommage, y"); } - abstract QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery); + 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 163a91dd95..66dbcca20d 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 @@ -78,7 +78,7 @@ void allowsShortJpaSyntax() { @ParameterizedTest @MethodSource("detectsAliasWithUCorrectlySource") - void detectsAliasWithUCorrectly(DeclaredQuery query, String alias) { + void detectsAliasWithUCorrectly(IntrospectedQuery query, String alias) { assumeThat(query.getQueryString()).as("JsqlParser does not support simple JPA syntax") .doesNotStartWithIgnoringCase("from"); @@ -186,8 +186,7 @@ void preserveSourceQueryWhenAddingSort() { true); assertThat(getEnhancer(query).applySorting(Sort.by("name"), "p")) // - .startsWithIgnoringCase(query.getQueryString()) - .endsWithIgnoringCase("ORDER BY p.name ASC"); + .startsWithIgnoringCase(query.getQueryString()).endsWithIgnoringCase("ORDER BY p.name ASC"); } @Test // GH-2812 @@ -433,7 +432,7 @@ void discoversAliasWithComplexFunction() { assertThat( QueryUtils.getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) // - .contains("myAlias"); + .contains("myAlias"); } @Test // DATAJPA-1506 @@ -538,7 +537,7 @@ void detectsAliasWithGroupAndOrderByWithLineBreaks() { @ParameterizedTest // DATAJPA-1679 @MethodSource("findProjectionClauseWithDistinctSource") - void findProjectionClauseWithDistinct(DeclaredQuery query, String expected) { + void findProjectionClauseWithDistinct(IntrospectedQuery query, String expected) { SoftAssertions.assertSoftly(sofly -> sofly.assertThat(getEnhancer(query).getProjection()).isEqualTo(expected)); } @@ -633,7 +632,8 @@ void modifyingQueriesAreDetectedCorrectly() { assertThat(modiQuery.hasConstructorExpression()).isEqualTo(constructorExpressionNotConsideringQueryType); assertThat(countQueryForNotConsiderQueryType).isEqualToIgnoringCase(modifyingQuery); - assertThat(QueryEnhancerFactory.forQuery(modiQuery).createCountQueryFor()).isEqualToIgnoringCase(modifyingQuery); + assertThat(QueryEnhancerFactory.forQuery(modiQuery).create(modiQuery).createCountQueryFor()) + .isEqualToIgnoringCase(modifyingQuery); } @ParameterizedTest // GH-2593 @@ -641,7 +641,7 @@ void modifyingQueriesAreDetectedCorrectly() { void insertStatementIsProcessedSameAsDefault(String insertQuery) { StringQuery stringQuery = new StringQuery(insertQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); Sort sorting = Sort.by("day").descending(); @@ -696,8 +696,8 @@ private static void assertCountQuery(StringQuery originalQuery, String countQuer assertThat(getEnhancer(originalQuery).createCountQueryFor()).isEqualToIgnoringCase(countQuery); } - private static QueryEnhancer getEnhancer(DeclaredQuery query) { - return QueryEnhancerFactory.forQuery(query); + private static QueryEnhancer getEnhancer(IntrospectedQuery query) { + return QueryEnhancerFactory.forQuery(query).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 4640443b99..34d3ab2397 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 @@ -52,7 +52,8 @@ void before() { @Test // DATAJPA-1058 void noExceptionWhenQueryDoesNotContainNamedParameters() { - setterFactory.create(binding); + setterFactory.create(binding, + EntityQuery.introspectJpql("from Employee e", QueryEnhancerSelector.DEFAULT_SELECTOR)); } @Test // DATAJPA-1058 @@ -61,28 +62,14 @@ void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() { when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter("NamedParameter", 1)); assertThatExceptionOfType(IllegalStateException.class) // - .isThrownBy(() -> setterFactory.create(binding - )) // + .isThrownBy(() -> setterFactory.create(binding, + EntityQuery.introspectJpql("from Employee e where e.name = :NamedParameter", + QueryEnhancerSelector.DEFAULT_SELECTOR))) // .withMessageContaining("Java 8") // .withMessageContaining("@Param") // .withMessageContaining("-parameters"); } - @Test // DATAJPA-1281 - void exceptionWhenCriteriaQueryContainsInsufficientAmountOfParameters() { - - // no parameter present in the criteria query - QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forPartTreeQuery(parameters); - - // one argument present in the method signature - when(binding.getRequiredPosition()).thenReturn(1); - when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); - - assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> setterFactory.create(binding)) // - .withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query"); - } - @Test // DATAJPA-1281 void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() { @@ -94,7 +81,10 @@ void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() { when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> setterFactory.create(binding)) // + .isThrownBy( + () -> setterFactory.create(binding, + EntityQuery.introspectJpql("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 4b53c362c3..5887eab53b 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 @@ -47,7 +47,6 @@ import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -75,6 +74,9 @@ @MockitoSettings(strictness = Strictness.LENIENT) class SimpleJpaQueryUnitTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + private static final String USER_QUERY = "select u from User u"; private JpaQueryMethod method; @@ -119,8 +121,7 @@ void prefersDeclaredCountQueryOverCreatingOne() throws Exception { extractor); when(em.createQuery("foo", Long.class)).thenReturn(typedQuery); - SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, - QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); + SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, CONFIG); assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {}))) .isEqualTo(typedQuery); @@ -134,8 +135,7 @@ void doesNotApplyPaginationToCountQuery() throws Exception { 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, - QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); + AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null, CONFIG); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -149,9 +149,8 @@ void discoversNativeQuery() throws Exception { Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); - AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, - queryMethod.getAnnotatedQuery(), null, QueryRewriter.IdentityQueryRewriter.INSTANCE, - ValueExpressionDelegate.create()); + AbstractJpaQuery jpaQuery = JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, + queryMethod.getAnnotatedQuery(), null, CONFIG); assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); @@ -169,9 +168,8 @@ void discoversNativeQueryFromNativeQueryInterface() throws Exception { Method method = SampleRepository.class.getMethod("findByLastnameNativeAnnotation", String.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); - AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, - queryMethod.getAnnotatedQuery(), null, QueryRewriter.IdentityQueryRewriter.INSTANCE, - ValueExpressionDelegate.create()); + AbstractJpaQuery jpaQuery = JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, + queryMethod.getAnnotatedQuery(), null, CONFIG); assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); @@ -239,10 +237,11 @@ void allowsCountQueryUsingParametersNotInOriginalQuery() throws Exception { when(em.createNativeQuery(anyString())).thenReturn(query); AbstractJpaQuery jpaQuery = createJpaQuery( - SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class), Optional.empty()); + SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class), + Optional.empty()); jpaQuery.doCreateCountQuery(new JpaParametersParameterAccessor(jpaQuery.getQueryMethod().getParameters(), - new Object[]{"data", PageRequest.of(0, 10)})); + new Object[] { "data", PageRequest.of(0, 10) })); ArgumentCaptor queryStringCaptor = ArgumentCaptor.forClass(String.class); verify(em).createQuery(queryStringCaptor.capture(), eq(Long.class)); @@ -283,8 +282,7 @@ void resolvesExpressionInCountQuery() throws Exception { 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", QueryRewriter.IdentityQueryRewriter.INSTANCE, - ValueExpressionDelegate.create()); + "select count(u.id) from #{#entityName} u", CONFIG); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -296,16 +294,18 @@ private AbstractJpaQuery createJpaQuery(Method method) { return createJpaQuery(method, null); } - private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) { + private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, + @Nullable String countQueryString) { - return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString, - QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); + return JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, queryString, + countQueryString, CONFIG); } 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.getAnnotatedQuery(), + countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery())); } interface SampleRepository { @@ -337,8 +337,8 @@ interface SampleRepository { @Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u") List findAllWithExpressionInCountQuery(Pageable pageable); - - @Query(value = "select u from User u", countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}") + @Query(value = "select u from User u", + countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}") List findAllWithBindingsOnlyInCountQuery(String arg0, Pageable pageable); // Typo in named parameter diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java index 41b36b21d7..134ae29417 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java @@ -911,12 +911,14 @@ void usingGreaterThanWithNamedParameter() { void checkNumberOfNamedParameters(String query, int expectedSize, String label, boolean nativeQuery) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, nativeQuery); + EntityQuery introspectedQuery = nativeQuery + ? EntityQuery.introspectNativeQuery(query, QueryEnhancerSelector.DEFAULT_SELECTOR) + : EntityQuery.introspectJpql(query, QueryEnhancerSelector.DEFAULT_SELECTOR); - assertThat(declaredQuery.hasNamedParameter()) // + assertThat(introspectedQuery.hasNamedParameter()) // .describedAs("hasNamed Parameter " + label) // .isEqualTo(expectedSize > 0); - assertThat(declaredQuery.getParameterBindings()) // + assertThat(introspectedQuery.getParameterBindings()) // .describedAs("parameterBindings " + label) // .hasSize(expectedSize); } From ee8d712580dc2ede03145c00537893b890e9b55f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 22 Jan 2025 14:52:04 +0100 Subject: [PATCH 3/7] Polishing. --- .../config/EnableJpaRepositories.java | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) 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 46ff8ed2d9..68a173f059 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 @@ -84,46 +84,39 @@ * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning * for {@code PersonRepositoryImpl}. - * - * @return */ String repositoryImplementationPostfix() default "Impl"; /** * Configures the location of where to find the Spring Data named queries properties file. Will default to * {@code META-INF/jpa-named-queries.properties}. - * - * @return */ String namedQueriesLocation() default ""; /** * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to * {@link Key#CREATE_IF_NOT_FOUND}. - * - * @return */ Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; /** * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to * {@link JpaRepositoryFactoryBean}. - * - * @return */ Class repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class; /** * Configure the repository base class to be used to create repository proxies for this particular configuration. * - * @return * @since 1.9 */ Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; /** * Configure a specific {@link BeanNameGenerator} to be used when creating the repository beans. - * @return the {@link BeanNameGenerator} to be used or the base {@link BeanNameGenerator} interface to indicate context default. + * + * @return the {@link BeanNameGenerator} to be used or the base {@link BeanNameGenerator} interface to indicate + * context default. * @since 3.4 */ Class nameGenerator() default BeanNameGenerator.class; @@ -133,22 +126,18 @@ /** * Configures the name of the {@link EntityManagerFactory} bean definition to be used to create repositories * discovered through this annotation. Defaults to {@code entityManagerFactory}. - * - * @return */ String entityManagerFactoryRef() default "entityManagerFactory"; /** * Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories * discovered through this annotation. Defaults to {@code transactionManager}. - * - * @return */ String transactionManagerRef() default "transactionManager"; /** * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the - * repositories infrastructure. + * repository infrastructure. */ boolean considerNestedRepositories() default false; @@ -170,7 +159,6 @@ * completed its bootstrap. {@link BootstrapMode#DEFERRED} is fundamentally the same as {@link BootstrapMode#LAZY}, * but triggers repository initialization when the application context finishes its bootstrap. * - * @return * @since 2.1 */ BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT; @@ -187,6 +175,7 @@ * Configures the {@link QueryEnhancerSelector} to select a query enhancer for query introspection and transformation. * * @return a {@link QueryEnhancerSelector} class providing a no-args constructor. + * @since 4.0 */ Class queryEnhancerSelector() default QueryEnhancerSelector.DefaultQueryEnhancerSelector.class; } From c2dde898d5c9063ddc2fe2c8248872dae5451906 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 4 Mar 2025 13:41:10 +0100 Subject: [PATCH 4/7] hacking --- .../repository/query/HqlParserBenchmarks.java | 2 +- .../JSqlParserQueryEnhancerBenchmarks.java | 2 +- .../jpa/repository/query/BindableQuery.java | 67 ++++++++++ .../jpa/repository/query/DeclaredQuery.java | 23 ++-- .../query/DefaultDeclaredQuery.java | 68 ---------- .../query/DefaultQueryEnhancer.java | 10 +- .../query/EmptyIntrospectedQuery.java | 10 ++ .../repository/query/IntrospectedQuery.java | 8 +- .../query/JSqlParserQueryEnhancer.java | 10 +- .../data/jpa/repository/query/JpqlQuery.java | 38 ++++++ .../data/jpa/repository/query/NamedQuery.java | 4 - .../jpa/repository/query/NativeQuery.java | 38 ++++++ .../query/ParameterBinderFactory.java | 4 +- .../jpa/repository/query/QueryEnhancer.java | 2 +- .../query/QueryEnhancerFactories.java | 10 +- .../query/QueryEnhancerFactory.java | 2 +- .../query/QueryEnhancerSelector.java | 2 +- .../jpa/repository/query/QueryString.java | 24 ++++ .../jpa/repository/query/StringQuery.java | 122 ++++++++---------- .../query/DefaultQueryEnhancerUnitTests.java | 2 +- .../ExpressionBasedStringQueryUnitTests.java | 6 +- .../JSqlParserQueryEnhancerUnitTests.java | 20 +-- ...rIndexedQueryParameterSetterUnitTests.java | 8 +- .../query/QueryEnhancerFactoryUnitTests.java | 4 +- .../query/QueryEnhancerTckTests.java | 8 +- .../query/QueryEnhancerUnitTests.java | 6 +- .../query/StringQueryUnitTests.java | 9 +- 27 files changed, 304 insertions(+), 205 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryString.java 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 fb524d76bf..ecbb4eb238 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 @@ -55,7 +55,7 @@ OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE "cost overrun" """; - query = DeclaredQuery.ofJpql(s); + query = DeclaredQuery.jpqlQuery(s); enhancer = QueryEnhancerFactory.forQuery(query).create(query); } } 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 aeb1764c5c..a5c9cdce23 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 @@ -56,7 +56,7 @@ public void doSetup() throws IOException { select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"""; - enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.ofNative(s)); + enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.nativeQuery(s)); } } 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 new file mode 100644 index 0000000000..66e95a93c5 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java @@ -0,0 +1,67 @@ +/* + * 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/DeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java index ca32d1f46b..8b9e9cde59 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 @@ -23,33 +23,28 @@ * @author Mark Paluch * @since 2.0.3 */ -public interface DeclaredQuery { +public sealed interface DeclaredQuery extends QueryString permits JpqlQuery, NativeQuery, BindableQuery { /** * Creates a DeclaredQuery for a JPQL query. * - * @param query the JPQL query string. - * @return + * @param jpql the JPQL query string. + * @return new instance of {@link DeclaredQuery}. */ - static DeclaredQuery ofJpql(String query) { - return new DefaultDeclaredQuery(query, false); + static DeclaredQuery jpqlQuery(String jpql) { + return new JpqlQuery(jpql); } /** * Creates a DeclaredQuery for a native query. * - * @param query the native query string. - * @return + * @param sql the native query string. + * @return new instance of {@link DeclaredQuery}. */ - static DeclaredQuery ofNative(String query) { - return new DefaultDeclaredQuery(query, true); + static DeclaredQuery nativeQuery(String sql) { + return new NativeQuery(sql); } - /** - * Returns the query string. - */ - String getQueryString(); - /** * Return whether the query is a native query of not. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java deleted file mode 100644 index a24512a994..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2024 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; - -/** - * @author Mark Paluch - */ -class DefaultDeclaredQuery implements DeclaredQuery { - - private final String query; - private final boolean nativeQuery; - - DefaultDeclaredQuery(String query, boolean nativeQuery) { - this.query = query; - this.nativeQuery = nativeQuery; - } - - @Override - public String getQueryString() { - return query; - } - - @Override - public boolean isNativeQuery() { - return nativeQuery; - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (!(object instanceof DefaultDeclaredQuery that)) { - return false; - } - if (nativeQuery != that.nativeQuery) { - return false; - } - return ObjectUtils.nullSafeEquals(query, that.query); - } - - @Override - public int hashCode() { - int result = ObjectUtils.nullSafeHashCode(query); - result = 31 * result + (nativeQuery ? 1 : 0); - return result; - } - - @Override - public String toString() { - return (isNativeQuery() ? "[native] " : "[JPQL] ") + getQueryString(); - } -} 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 1fe6236621..fc21b51fbb 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 @@ -29,13 +29,13 @@ */ public class DefaultQueryEnhancer implements QueryEnhancer { - private final DeclaredQuery query; + private final QueryString query; private final boolean hasConstructorExpression; private final @Nullable String alias; private final String projection; private final Set joinAliases; - public DefaultQueryEnhancer(DeclaredQuery query) { + public DefaultQueryEnhancer(QueryString query) { this.query = query; this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString()); this.alias = QueryUtils.detectAlias(query.getQueryString()); @@ -60,7 +60,9 @@ public String rewrite(QueryRewriteInformation rewriteInformation) { @Override public String createCountQueryFor(@Nullable String countProjection) { - return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery()); + + boolean nativeQuery = this.query instanceof DeclaredQuery dc ? dc.isNativeQuery() : true; + return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, nativeQuery); } @Override @@ -84,7 +86,7 @@ public Set getJoinAliases() { } @Override - public DeclaredQuery getQuery() { + public QueryString 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 c51f0c4ca4..ec92ee81cf 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 @@ -63,6 +63,11 @@ public boolean isDefaultProjection() { return false; } + @Override + public String getQueryString() { + return ""; + } + @Override public List getParameterBindings() { return Collections.emptyList(); @@ -82,4 +87,9 @@ public String applySorting(Sort sort) { 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/IntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java index 427dbcc03b..8ad4ffc0d7 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/IntrospectedQuery.java @@ -24,7 +24,13 @@ * @author Diego Krupitza * @since 2.0.3 */ -interface IntrospectedQuery extends DeclaredQuery { +interface IntrospectedQuery extends QueryString { + + DeclaredQuery getDeclaredQuery(); + + default String getQueryString() { + return getDeclaredQuery().getQueryString(); + } /** * @return whether the underlying query has at least one named parameter. 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 f68443adda..fb4996ae72 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 @@ -70,7 +70,7 @@ */ public class JSqlParserQueryEnhancer implements QueryEnhancer { - private final DeclaredQuery query; + private final QueryString query; private final Statement statement; private final ParsedType parsedType; private final boolean hasConstructorExpression; @@ -83,7 +83,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { /** * @param query the query we want to enhance. Must not be {@literal null}. */ - public JSqlParserQueryEnhancer(DeclaredQuery query) { + public JSqlParserQueryEnhancer(QueryString query) { this.query = query; this.statement = parseStatement(query.getQueryString(), Statement.class); @@ -295,7 +295,7 @@ public Set getSelectionAliases() { } @Override - public DeclaredQuery getQuery() { + public QueryString getQuery() { return this.query; } @@ -373,8 +373,8 @@ public String createCountQueryFor(@Nullable String countProjection) { return createCountQueryFor(selectBody, countProjection, primaryAlias); } - private static String createCountQueryFor(PlainSelect selectBody, @Nullable String countProjection, - @Nullable String primaryAlias) { + private static String createCountQueryFor(QueryString query, 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/JpqlQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java new file mode 100644 index 0000000000..6a8f3cce03 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java @@ -0,0 +1,38 @@ +/* + * 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 + */ +final class JpqlQuery implements DeclaredQuery { + + private final String jpql; + + JpqlQuery(String jpql) { + this.jpql = jpql; + } + + @Override + public boolean isNativeQuery() { + return false; + } + + @Override + public String getQueryString() { + return jpql; + } +} 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 2263f4d975..83002d326f 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 @@ -181,15 +181,11 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc EntityManager em = getEntityManager(); TypedQuery countQuery; - String cacheKey; if (namedCountQueryIsPresent) { - cacheKey = countQueryName; countQuery = em.createNamedQuery(countQueryName, Long.class); - } else { String countQueryString = entityQuery.get().deriveCountQuery(countProjection).getQueryString(); - cacheKey = countQueryString; countQuery = em.createQuery(countQueryString, Long.class); } 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/NativeQuery.java new file mode 100644 index 0000000000..6ba9f81ba6 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java @@ -0,0 +1,38 @@ +/* + * 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 + */ +final class NativeQuery implements DeclaredQuery { + + private final String sql; + + 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/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index 8abf7d461d..fc34606f45 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 @@ -92,7 +92,6 @@ static ParameterBinder createQueryAwareBinder(JpaParameters parameters, Introspe Assert.notNull(parser, "SpelExpressionParser must not be null"); Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null"); - List bindings = query.getParameterBindings(); QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser, evaluationContextProvider); @@ -101,7 +100,8 @@ static ParameterBinder createQueryAwareBinder(JpaParameters parameters, Introspe boolean usesPaging = query instanceof EntityQuery eq && eq.usesPaging(); - return new ParameterBinder(parameters, createSetters(bindings, query, expressionSetterFactory, basicSetterFactory), + // TODO: lets maybe obtain the bindable query and pass that on to create the setters? + return new ParameterBinder(parameters, createSetters(query.getParameterBindings(), query, expressionSetterFactory, basicSetterFactory), !usesPaging); } 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 ff9f44c44a..a5eabfb343 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 @@ -66,7 +66,7 @@ public interface QueryEnhancer { * * @return non-null {@link DeclaredQuery} that wraps the query. */ - DeclaredQuery getQuery(); + QueryString getQuery(); /** * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to. 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 b88a6953f0..9d23c838db 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 @@ -57,7 +57,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(QueryString query) { return new DefaultQueryEnhancer(query); } }, @@ -69,7 +69,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(QueryString query) { if (jSqlParserPresent) { return new JSqlParserQueryEnhancer(query); } @@ -85,7 +85,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(QueryString query) { return JpaQueryEnhancer.forHql(query.getQueryString()); } }, @@ -96,7 +96,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(QueryString query) { return JpaQueryEnhancer.forEql(query.getQueryString()); } }, @@ -107,7 +107,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(QueryString query) { return JpaQueryEnhancer.forJpql(query.getQueryString()); } } 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 a3e7b5f06d..7ba1bf6e37 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 @@ -40,7 +40,7 @@ public interface QueryEnhancerFactory { * @param query the query to be enhanced and introspected. * @return */ - QueryEnhancer create(DeclaredQuery query); + QueryEnhancer create(QueryString 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 75bee83f1d..93268c6387 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 @@ -66,7 +66,7 @@ class DefaultQueryEnhancerSelector implements QueryEnhancerSelector { private final QueryEnhancerFactory nativeQuery; private final QueryEnhancerFactory jpql; - public DefaultQueryEnhancerSelector() { + DefaultQueryEnhancerSelector() { this(DEFAULT_NATIVE, DEFAULT_JPQL); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryString.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryString.java new file mode 100644 index 0000000000..536add69e6 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryString.java @@ -0,0 +1,24 @@ +/* + * 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 QueryString { + + String getQueryString(); +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index 39af6fb1e3..e15bcb2e33 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -64,11 +64,8 @@ */ class StringQuery implements EntityQuery { - private final String query; - private final List bindings; + private final BindableQuery bindableQuery; private final boolean containsPageableInSpel; - private final boolean usesJdbcStyleParameters; - private final boolean isNative; private final QueryEnhancerFactory queryEnhancerFactory; private final QueryEnhancer queryEnhancer; private final boolean hasNamedParameters; @@ -78,7 +75,7 @@ class StringQuery implements EntityQuery { * * @param query must not be {@literal null} or empty. */ - public StringQuery(String query, boolean isNative) { + StringQuery(String query, boolean isNative) { this(query, isNative, QueryEnhancerSelector.DEFAULT_SELECTOR, it -> {}); } @@ -91,29 +88,15 @@ public StringQuery(String query, boolean isNative) { Assert.hasText(query, "Query must not be null or empty"); - this.isNative = isNative; - this.bindings = new ArrayList<>(); this.containsPageableInSpel = query.contains("#pageable"); this.queryEnhancerFactory = factory; - Metadata queryMeta = new Metadata(); - this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, - this.bindings, queryMeta); + DeclaredQuery source = isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); + this.bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); - this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; - this.queryEnhancer = factory.create(this); - - parameterPostProcessor.accept(this.bindings); - - boolean hasNamedParameters = false; - for (ParameterBinding parameterBinding : getParameterBindings()) { - if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { - hasNamedParameters = true; - break; - } - } - - this.hasNamedParameters = hasNamedParameters; + parameterPostProcessor.accept(this.bindableQuery.getBindings()); + this.queryEnhancer = factory.create(this.bindableQuery); + this.hasNamedParameters = containsNamedParameter(this.bindableQuery.getBindings()); } /** @@ -125,29 +108,32 @@ public StringQuery(String query, boolean isNative) { Assert.hasText(query, "Query must not be null or empty"); - this.isNative = isNative; - this.bindings = new ArrayList<>(); this.containsPageableInSpel = query.contains("#pageable"); + DeclaredQuery source = isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); - Metadata queryMeta = new Metadata(); - this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, - this.bindings, queryMeta); - - this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; - this.queryEnhancerFactory = selector.select(this); - this.queryEnhancer = queryEnhancerFactory.create(this); + this.bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); - parameterPostProcessor.accept(this.bindings); - - boolean hasNamedParameters = false; - for (ParameterBinding parameterBinding : getParameterBindings()) { - if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { - hasNamedParameters = true; - break; - } - } + 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; + this.containsPageableInSpel = containsPageableInSpel; } QueryEnhancer getQueryEnhancer() { @@ -158,16 +144,21 @@ QueryEnhancer getQueryEnhancer() { * Returns whether we have found some like bindings. */ boolean hasParameterBindings() { - return !bindings.isEmpty(); + return this.bindableQuery.hasBindings(); } String getProjection() { return this.queryEnhancer.getProjection(); } + @Override + public String getQueryString() { + return bindableQuery.getQueryString(); + } + @Override public List getParameterBindings() { - return bindings; + return this.bindableQuery.getBindings(); } @Override @@ -177,14 +168,14 @@ public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) // JPA parameter markers and not the original expressions anymore. return new StringQuery(this.queryEnhancer.createCountQueryFor(countQueryProjection), // - this.isNative, queryEnhancerFactory, derivedBindings -> { + 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 : bindings) { + for (ParameterBinding binding : getParameterBindings()) { Predicate identifier = binding::bindsTo; Predicate notCompatible = Predicate.not(binding::isCompatibleWith); @@ -206,12 +197,7 @@ public String applySorting(Sort sort) { @Override public boolean usesJdbcStyleParameters() { - return usesJdbcStyleParameters; - } - - @Override - public String getQueryString() { - return query; + return bindableQuery.usesJdbcStyleParameters(); } public @Nullable String getAlias() { @@ -239,8 +225,17 @@ public boolean usesPaging() { } @Override - public boolean isNativeQuery() { - return isNative; + public DeclaredQuery getDeclaredQuery() { + return bindableQuery; + } + + private static boolean containsNamedParameter(List bindings) { + for (ParameterBinding parameterBinding : bindings) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { + return true; + } + } + return false; } /** @@ -376,8 +371,7 @@ enum ParameterBindingParser { * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns * the cleaned up query. */ - String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List bindings, - Metadata queryMeta) { + BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(DeclaredQuery query) { IndexedParameterLabels parameterLabels = new IndexedParameterLabels(findParameterIndices(query)); boolean parametersShouldBeAccessedByIndex = parameterLabels.hasLabels(); @@ -385,11 +379,11 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que /* * Prefer indexed access over named parameters if only SpEL Expression parameters are present. */ - if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) { + if (!parametersShouldBeAccessedByIndex && query.getQueryString().contains("?#{")) { parametersShouldBeAccessedByIndex = true; } - ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query, + ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query.getQueryString(), parametersShouldBeAccessedByIndex, parameterLabels); String resultingQuery = parsedQuery.getQueryString(); @@ -412,14 +406,14 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que String match = matcher.group(0); if (JDBC_STYLE_PARAM.matcher(match).find()) { - queryMeta.usesJdbcStyleParameters = true; + jdbcStyle = true; } if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) { usesJpaStyleParameters = true; } - if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) { + if (usesJpaStyleParameters && jdbcStyle) { throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported"); } @@ -467,7 +461,7 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que } replacement = targetBinding.hasName() ? ":" + targetBinding.getName() - : ((!usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) ? "?" + : ((!usesJpaStyleParameters && jdbcStyle) ? "?" : "?" + targetBinding.getPosition()); String result; String substring = matcher.group(2); @@ -484,7 +478,7 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que resultingQuery = result; } - return resultingQuery; + return new BindableQuery(query, resultingQuery, bindings, jdbcStyle); } private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, @@ -592,9 +586,7 @@ static ParameterBindingType of(String typeSource) { } } - static class Metadata { - private boolean usesJdbcStyleParameters = false; - } + /** * Utility to create unique parameter bindings for LIKE that refer to the same underlying method parameter but are 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 e0488df118..9a5c9ff30f 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 @@ -43,7 +43,7 @@ void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {} @Test // GH-3546 void shouldApplySorting() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative("SELECT e FROM Employee e")); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery("SELECT e FROM Employee e")); String sql = enhancer.applySorting(Sort.by("foo", "bar")); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java index 8e8528a4bd..a235543017 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java @@ -111,7 +111,7 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { + "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})", metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); - assertThat(query.isNativeQuery()).isFalse(); + assertThat(query.getDeclaredQuery().isNativeQuery()).isFalse(); } @Test @@ -120,7 +120,7 @@ void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); - assertThat(query.isNativeQuery()).isFalse(); + assertThat(query.getDeclaredQuery().isNativeQuery()).isFalse(); } @Test @@ -129,7 +129,7 @@ void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); - assertThat(query.isNativeQuery()).isTrue(); + assertThat(query.getDeclaredQuery().isNativeQuery()).isTrue(); } @Test // GH-3041 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 a3977b8a64..52787f910f 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 @@ -44,7 +44,7 @@ QueryEnhancer createQueryEnhancer(DeclaredQuery query) { @Test // GH-3546 void shouldApplySorting() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofJpql("SELECT e FROM Employee e")); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.jpqlQuery("SELECT e FROM Employee e")); String sql = enhancer.applySorting(Sort.by("foo", "bar")); @@ -54,7 +54,7 @@ void shouldApplySorting() { @Test // GH-3707 void countQueriesShouldConsiderPrimaryTableAlias() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(""" + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(""" SELECT DISTINCT a.*, b.b1 FROM TableA a JOIN TableB b ON a.b = b.b @@ -83,7 +83,7 @@ void setOperationListWorks() { + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); @@ -106,7 +106,7 @@ void complexSetOperationListWorks() { + "union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); @@ -133,7 +133,7 @@ void deeplyNestedcomplexSetOperationListWorks() { + "\t;"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("CustomerID"); @@ -153,7 +153,7 @@ void valuesStatementsWorks() { String setQuery = "VALUES (1, 2, 'test')"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isNullOrEmpty(); @@ -174,7 +174,7 @@ void withStatementsWorks() { + "select day, value from sample_data as a"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); @@ -197,7 +197,7 @@ void multipleWithStatementsWorks() { + "select day, value from sample_data as a"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); @@ -217,7 +217,7 @@ void multipleWithStatementsWorks() { void truncateStatementShouldWork() { StringQuery stringQuery = new StringQuery("TRUNCATE TABLE foo", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNull(); assertThat(stringQuery.getProjection()).isEmpty(); @@ -235,7 +235,7 @@ void truncateStatementShouldWork() { void mergeStatementWorksWithJSqlParser(String query, String alias) { StringQuery stringQuery = new StringQuery(query, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); assertThat(QueryUtils.detectAlias(query)).isNull(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java index e85ff114f1..d438cdf9a6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java @@ -89,7 +89,7 @@ void strictErrorHandlingThrowsExceptionForAllVariationsOfParameters() { softly .assertThatThrownBy( - () -> setter.setParameter(BindableQuery.from(query), methodArguments, STRICT)) // + () -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, STRICT)) // .describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", // parameter.getClass(), // parameter.getName(), // @@ -118,7 +118,7 @@ void lenientErrorHandlingThrowsNoExceptionForAllVariationsOfParameters() { softly .assertThatCode( - () -> setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT)) // + () -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT)) // .describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", // parameter.getClass(), // parameter.getName(), // @@ -149,7 +149,7 @@ void lenientSetsParameterWhenSuccessIsUnsure() { temporalType // ); - setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT); + setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT); if (temporalType == null) { verify(query).setParameter(eq(11), any(Date.class)); @@ -179,7 +179,7 @@ void parameterNotSetWhenSuccessImpossible() { temporalType // ); - setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT); + setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT); if (temporalType == null) { verify(query, never()).setParameter(anyInt(), any(Date.class)); 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 7456e047c2..aaccc4cad4 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 @@ -34,7 +34,7 @@ void createsParsingImplementationForNonNativeQuery() { StringQuery query = new StringQuery("select new com.example.User(u.firstname) from User u", false); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query); assertThat(queryEnhancer) // .isInstanceOf(JpaQueryEnhancer.class); @@ -49,7 +49,7 @@ void createsJSqlImplementationForNativeQuery() { StringQuery query = new StringQuery("select * from User", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).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 4b4bb8dfe0..7a0f4e1783 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 @@ -35,7 +35,7 @@ abstract class QueryEnhancerTckTests { @MethodSource("nativeCountQueries") // GH-2773 void shouldDeriveNativeCountQuery(String query, String expected) { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(query)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(query)); String countQueryFor = enhancer.createCountQueryFor(); // lenient cleanup to allow for rendering variance @@ -119,7 +119,7 @@ static Stream nativeCountQueries() { @MethodSource("jpqlCountQueries") void shouldDeriveJpqlCountQuery(String query, String expected) { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofJpql(query)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.jpqlQuery(query)); String countQueryFor = enhancer.createCountQueryFor(null); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -178,7 +178,7 @@ static Stream jpqlCountQueries() { @MethodSource("nativeQueriesWithVariables") void shouldDeriveNativeCountQueryWithVariable(String query, String expected) { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(query)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(query)); String countQueryFor = enhancer.createCountQueryFor(); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -205,7 +205,7 @@ void findProjectionClauseWithIncludedFrom() { StringQuery query = new StringQuery("select x, frommage, y from t", true); - assertThat(createQueryEnhancer(query).getProjection()).isEqualTo("x, frommage, y"); + assertThat(createQueryEnhancer(query.getDeclaredQuery()).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 66dbcca20d..0e5f44cd8b 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 @@ -632,7 +632,7 @@ void modifyingQueriesAreDetectedCorrectly() { assertThat(modiQuery.hasConstructorExpression()).isEqualTo(constructorExpressionNotConsideringQueryType); assertThat(countQueryForNotConsiderQueryType).isEqualToIgnoringCase(modifyingQuery); - assertThat(QueryEnhancerFactory.forQuery(modiQuery).create(modiQuery).createCountQueryFor()) + assertThat(QueryEnhancerFactory.forQuery(modiQuery.getDeclaredQuery()).create(modiQuery.getDeclaredQuery()).createCountQueryFor()) .isEqualToIgnoringCase(modifyingQuery); } @@ -641,7 +641,7 @@ void modifyingQueriesAreDetectedCorrectly() { void insertStatementIsProcessedSameAsDefault(String insertQuery) { StringQuery stringQuery = new StringQuery(insertQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery.getDeclaredQuery()); Sort sorting = Sort.by("day").descending(); @@ -697,7 +697,7 @@ private static void assertCountQuery(StringQuery originalQuery, String countQuer } private static QueryEnhancer getEnhancer(IntrospectedQuery query) { - return QueryEnhancerFactory.forQuery(query).create(query); + return QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query.getDeclaredQuery()); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java index 134ae29417..3c18eda1fb 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java @@ -17,7 +17,6 @@ import static org.assertj.core.api.Assertions.*; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -29,6 +28,7 @@ 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; /** @@ -925,11 +925,10 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) { - List bindings = new ArrayList<>(); - StringQuery.ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, - bindings, new StringQuery.Metadata()); + DeclaredQuery source = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); + BindableQuery bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); - assertThat(bindings.stream().anyMatch(it -> it.getIdentifier().hasName())) // + assertThat(bindableQuery.getBindings().stream().anyMatch(it -> it.getIdentifier().hasName())) // .describedAs(String.format("<%s> (%s)", query, label)) // .isEqualTo(expected); } From 9c9bc56b78f4d16c38e5c037eae7c8bfdf9eed98 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 7 Mar 2025 08:18:14 +0100 Subject: [PATCH 5/7] Rename QueryString to StructuredQuery & remove interface sealing --- .../data/jpa/repository/query/DeclaredQuery.java | 2 +- .../jpa/repository/query/DefaultQueryEnhancer.java | 6 +++--- .../data/jpa/repository/query/IntrospectedQuery.java | 2 +- .../jpa/repository/query/JSqlParserQueryEnhancer.java | 8 ++++---- .../data/jpa/repository/query/QueryEnhancer.java | 2 +- .../jpa/repository/query/QueryEnhancerFactories.java | 10 +++++----- .../jpa/repository/query/QueryEnhancerFactory.java | 2 +- .../query/{QueryString.java => StructuredQuery.java} | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{QueryString.java => StructuredQuery.java} (95%) 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 8b9e9cde59..152c40c385 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 @@ -23,7 +23,7 @@ * @author Mark Paluch * @since 2.0.3 */ -public sealed interface DeclaredQuery extends QueryString permits JpqlQuery, NativeQuery, BindableQuery { +public interface DeclaredQuery extends StructuredQuery { /** * Creates a DeclaredQuery for a JPQL query. 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 fc21b51fbb..3d4aba2859 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 @@ -29,13 +29,13 @@ */ public class DefaultQueryEnhancer implements QueryEnhancer { - private final QueryString query; + private final StructuredQuery query; private final boolean hasConstructorExpression; private final @Nullable String alias; private final String projection; private final Set joinAliases; - public DefaultQueryEnhancer(QueryString query) { + public DefaultQueryEnhancer(StructuredQuery query) { this.query = query; this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString()); this.alias = QueryUtils.detectAlias(query.getQueryString()); @@ -86,7 +86,7 @@ public Set getJoinAliases() { } @Override - public QueryString getQuery() { + public StructuredQuery getQuery() { return this.query; } } 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/IntrospectedQuery.java index 8ad4ffc0d7..4a29bce6c8 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/IntrospectedQuery.java @@ -24,7 +24,7 @@ * @author Diego Krupitza * @since 2.0.3 */ -interface IntrospectedQuery extends QueryString { +interface IntrospectedQuery extends StructuredQuery { DeclaredQuery getDeclaredQuery(); 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 fb4996ae72..8e052c9eec 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 @@ -70,7 +70,7 @@ */ public class JSqlParserQueryEnhancer implements QueryEnhancer { - private final QueryString query; + private final StructuredQuery query; private final Statement statement; private final ParsedType parsedType; private final boolean hasConstructorExpression; @@ -83,7 +83,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { /** * @param query the query we want to enhance. Must not be {@literal null}. */ - public JSqlParserQueryEnhancer(QueryString query) { + public JSqlParserQueryEnhancer(StructuredQuery query) { this.query = query; this.statement = parseStatement(query.getQueryString(), Statement.class); @@ -295,7 +295,7 @@ public Set getSelectionAliases() { } @Override - public QueryString getQuery() { + public StructuredQuery getQuery() { return this.query; } @@ -373,7 +373,7 @@ public String createCountQueryFor(@Nullable String countProjection) { return createCountQueryFor(selectBody, countProjection, primaryAlias); } - private static String createCountQueryFor(QueryString query, PlainSelect selectBody, + private static String createCountQueryFor(StructuredQuery query, PlainSelect selectBody, @Nullable String countProjection, @Nullable String primaryAlias) { // remove order by 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 a5eabfb343..528426f82f 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 @@ -66,7 +66,7 @@ public interface QueryEnhancer { * * @return non-null {@link DeclaredQuery} that wraps the query. */ - QueryString getQuery(); + StructuredQuery getQuery(); /** * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to. 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 9d23c838db..07ed8642c3 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 @@ -57,7 +57,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(QueryString query) { + public QueryEnhancer create(StructuredQuery query) { return new DefaultQueryEnhancer(query); } }, @@ -69,7 +69,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(QueryString query) { + public QueryEnhancer create(StructuredQuery query) { if (jSqlParserPresent) { return new JSqlParserQueryEnhancer(query); } @@ -85,7 +85,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(QueryString query) { + public QueryEnhancer create(StructuredQuery query) { return JpaQueryEnhancer.forHql(query.getQueryString()); } }, @@ -96,7 +96,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(QueryString query) { + public QueryEnhancer create(StructuredQuery query) { return JpaQueryEnhancer.forEql(query.getQueryString()); } }, @@ -107,7 +107,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(QueryString query) { + public QueryEnhancer create(StructuredQuery query) { return JpaQueryEnhancer.forJpql(query.getQueryString()); } } 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 7ba1bf6e37..26bdf4b5b2 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 @@ -40,7 +40,7 @@ public interface QueryEnhancerFactory { * @param query the query to be enhanced and introspected. * @return */ - QueryEnhancer create(QueryString query); + QueryEnhancer create(StructuredQuery 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/QueryString.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java similarity index 95% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryString.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java index 536add69e6..2ebfcb0549 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryString.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java @@ -18,7 +18,7 @@ /** * @author Christoph Strobl */ -public interface QueryString { +public interface StructuredQuery { String getQueryString(); } From ef17ae99177c97e1b05e7d668470469cd9218267 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 17 Mar 2025 15:19:59 +0100 Subject: [PATCH 6/7] 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. --- .../repository/query/HqlParserBenchmarks.java | 7 +- .../JSqlParserQueryEnhancerBenchmarks.java | 7 +- .../config/EnableJpaRepositories.java | 1 + .../query/AbstractStringBasedJpaQuery.java | 102 ++-- .../jpa/repository/query/BindableQuery.java | 67 --- .../jpa/repository/query/DeclaredQueries.java | 148 ++++++ .../jpa/repository/query/DeclaredQuery.java | 41 +- .../repository/query/DefaultEntityQuery.java | 158 ++++++ .../query/DefaultQueryEnhancer.java | 32 +- .../query/EmptyIntrospectedQuery.java | 53 +- .../jpa/repository/query/EntityQuery.java | 83 ++-- .../repository/query/IntrospectedQuery.java | 59 --- .../query/JSqlParserQueryEnhancer.java | 25 +- .../query/JpaQueryConfiguration.java | 1 + .../repository/query/JpaQueryEnhancer.java | 61 +-- .../query/JpaQueryLookupStrategy.java | 35 +- .../jpa/repository/query/JpaQueryMethod.java | 54 ++- .../data/jpa/repository/query/NamedQuery.java | 13 +- .../jpa/repository/query/NativeJpaQuery.java | 33 +- .../query/ParameterBinderFactory.java | 10 +- ...tringQuery.java => ParametrizedQuery.java} | 451 ++++++++---------- .../jpa/repository/query/QueryEnhancer.java | 63 +-- .../query/QueryEnhancerFactories.java | 18 +- .../query/QueryEnhancerFactory.java | 8 +- .../query/QueryEnhancerSelector.java | 4 +- .../query/QueryParameterSetterFactory.java | 14 +- .../{NativeQuery.java => QueryProvider.java} | 29 +- .../data/jpa/repository/query/QueryUtils.java | 6 +- .../jpa/repository/query/SimpleJpaQuery.java | 14 +- .../jpa/repository/query/StructuredQuery.java | 43 +- ...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 | 130 ++--- .../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 +- 51 files changed, 1338 insertions(+), 1159 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 delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{StringQuery.java => ParametrizedQuery.java} (63%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{NativeQuery.java => QueryProvider.java} (63%) 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 ecbb4eb238..d1465ed1bc 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.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 static class BenchmarkParameters { DeclaredQuery query; Sort sort = Sort.by("foo"); QueryEnhancer enhancer; + QueryEnhancer.QueryRewriteInformation rewriteInformation; @Setup(Level.Iteration) public void doSetup() { @@ -57,12 +60,14 @@ OR TREAT(p AS SmallProject).name LIKE 'Persist%' 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 a5c9cdce23..f4121c28ed 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.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 static class BenchmarkParameters { 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 void doSetup() throws IOException { 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/config/EnableJpaRepositories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java index 68a173f059..22f32ed2de 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 @@ * @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 be83fd2dc2..3510dbf9c5 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 @@ -20,9 +20,9 @@ import java.util.Objects; -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; @@ -32,7 +32,6 @@ import org.springframework.data.util.Lazy; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; -import org.springframework.util.StringUtils; /** * Base class for {@link String} based JPA queries. @@ -49,8 +48,8 @@ */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { - private final StringQuery query; - private final Lazy countQuery; + private final EntityQuery query; + private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; @@ -64,25 +63,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()); @@ -108,21 +124,25 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri "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 = processor.getReturnedType(); - 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. return parameterBinder.get().bindAndPrepare(query, accessor); } - String getSortedQueryString(Sort sort, ReturnedType returnedType) { + QueryProvider getSortedQuery(Sort sort, ReturnedType returnedType) { return querySortRewriter.getSorted(query, sort, returnedType); } @@ -131,7 +151,7 @@ protected ParameterBinder createBinder() { return createBinder(query); } - protected ParameterBinder createBinder(IntrospectedQuery query) { + protected ParameterBinder createBinder(StructuredQuery query) { return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, valueExpressionDelegate, valueExpressionContextProvider); } @@ -162,7 +182,7 @@ public EntityQuery getQuery() { /** * @return the countQuery */ - public IntrospectedQuery getCountQuery() { + public StructuredQuery getCountQuery() { return countQuery.get(); } @@ -170,20 +190,20 @@ public IntrospectedQuery getCountQuery() { * 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(); if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) { - return em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable)); + return em.createQuery(potentiallyRewriteQuery(query.getQueryString(), sort, pageable)); } Class typeToRead = getTypeToRead(returnedType); return typeToRead == null // - ? em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable)) // - : em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable), typeToRead); + ? em.createQuery(potentiallyRewriteQuery(query.getQueryString(), sort, pageable)) // + : em.createQuery(potentiallyRewriteQuery(query.getQueryString(), sort, pageable), typeToRead); } /** @@ -202,8 +222,8 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla : 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())); } @@ -211,7 +231,7 @@ String applySorting(CachableQuery cachableQuery) { * Query Sort Rewriter interface. */ interface QuerySortRewriter { - String getSorted(StringQuery query, Sort sort, ReturnedType returnedType); + QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType); } /** @@ -221,28 +241,28 @@ enum SimpleQuerySortRewriter implements QuerySortRewriter { 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; } } @@ -251,22 +271,22 @@ public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) */ 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)); @@ -282,12 +302,12 @@ public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) */ 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(); @@ -295,7 +315,7 @@ static class CachableQuery { 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 66e95a93c5..0000000000 --- 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 0000000000..2f6db9c5f7 --- /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 152c40c385..2cea734dbc 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 @@ /** * 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 @@ static DeclaredQuery jpqlQuery(String jpql) { * @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 0000000000..5d8807654c --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java @@ -0,0 +1,158 @@ +/* + * 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 ParametrizedQuery 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 ParametrizedQuery query; + private final QueryEnhancer queryEnhancer; + + DefaultEntityQuery(ParametrizedQuery 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()); + } + + @Override + public boolean usesPaging() { + return query.containsPageableInSpel(); + } + + public @Nullable String getAlias() { + return queryEnhancer.detectAlias(); + } + + String getProjection() { + return this.queryEnhancer.getProjection(); + } + + @Override + public StructuredQuery deriveCountQuery(@Nullable String countQueryProjection) { + return new SimpleStructuredQuery(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 StructuredQuery} variant forwarding to {@link ParametrizedQuery}. + */ + static class SimpleStructuredQuery implements StructuredQuery { + + private final ParametrizedQuery query; + + SimpleStructuredQuery(ParametrizedQuery 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 3d4aba2859..456c3139b3 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 @@ * @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 String rewrite(QueryRewriteInformation rewriteInformation) { @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 String getProjection() { } @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 ec92ee81cf..7ab38bbe06 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,39 +18,43 @@ 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 StructuredQuery}. * * @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 hasNamedParameter() { + public boolean hasParameterBindings() { return false; } @Override - public String getQueryString() { - return ""; + public boolean usesJdbcStyleParameters() { + return false; } - public @Nullable String getAlias() { - return null; + @Override + public boolean hasNamedParameter() { + return false; } @Override - public boolean isNativeQuery() { - return false; + public List getParameterBindings() { + return Collections.emptyList(); + } + + public @Nullable String getAlias() { + return null; } @Override @@ -69,27 +73,18 @@ public String getQueryString() { } @Override - public List getParameterBindings() { - return Collections.emptyList(); - } - - @Override - public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) { - return EMPTY_QUERY; + public StructuredQuery deriveCountQuery(@Nullable String countQueryProjection) { + return INSTANCE; } @Override - public String applySorting(Sort sort) { - return ""; + public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation) { + return this; } @Override - public boolean usesJdbcStyleParameters() { - return false; + public String toString() { + return ""; } - @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 b959d3810e..45e6ba5021 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 StructuredQuery} 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 StructuredQuery { /** - * 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 -> {}); - } - - /** - * 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 -> {}); - } + ParametrizedQuery preparsed = ParametrizedQuery.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, QueryEnhancerSelector selector) { - return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY - : new StringQuery(query, true, selector, parameterBindings -> {}); + return new DefaultEntityQuery(preparsed, enhancerFactory); } /** @@ -84,6 +57,14 @@ static EntityQuery introspectNativeQuery(String query, QueryEnhancerSelector sel */ boolean isDefaultProjection(); + /** + * @return whether paging is implemented in the query itself, e.g. using SpEL expressions. + * @since 2.0.6 + */ + default boolean usesPaging() { + 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 @@ -92,16 +73,16 @@ static EntityQuery introspectNativeQuery(String query, QueryEnhancerSelector sel * @param countQueryProjection an optional return type for the query. * @return a new {@literal IntrospectedQuery} instance. */ - IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection); - - String applySorting(Sort sort); + StructuredQuery deriveCountQuery(@Nullable String countQueryProjection); /** - * @return whether paging is implemented in the query itself, e.g. using SpEL expressions. - * @since 2.0.6 + * 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. */ - default boolean usesPaging() { - return false; - } + QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation); } 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/IntrospectedQuery.java deleted file mode 100644 index 4a29bce6c8..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2018-2024 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; - -/** - * A wrapper for a String representation of a query offering information about the query. - * - * @author Jens Schauder - * @author Diego Krupitza - * @since 2.0.3 - */ -interface IntrospectedQuery extends StructuredQuery { - - DeclaredQuery getDeclaredQuery(); - - default String getQueryString() { - return getDeclaredQuery().getQueryString(); - } - - /** - * @return whether the underlying query has at least one named 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(); - - /** - * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or - * name. - * - * @return Whether the query uses JDBC style parameters. - * @since 2.0.6 - */ - boolean usesJdbcStyleParameters(); - -} 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 8e052c9eec..82cae525c1 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 @@ -70,7 +70,7 @@ */ 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; @@ -83,7 +83,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); @@ -285,35 +285,20 @@ public String getProjection() { 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"); @@ -373,8 +358,8 @@ public String createCountQueryFor(@Nullable String countProjection) { return createCountQueryFor(selectBody, countProjection, primaryAlias); } - 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 7bce8dc8f7..788c977f25 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 EscapeCharacter getEscapeCharacter() { 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 ff4b6efb7d..b9b8a72b43 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.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 @@ static void configureParser(String query, String grammar, Lexer lexer, Parser pa } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using JPQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link StructuredQuery} using JPQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using JPQL. @@ -152,7 +150,7 @@ public static JpaQueryEnhancer forJpql(String query) { } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using HQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link StructuredQuery} using HQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using HQL. @@ -162,7 +160,7 @@ public static JpaQueryEnhancer forHql(String query) { } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using EQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link StructuredQuery} using EQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using EQL. @@ -197,8 +195,7 @@ public boolean hasConstructorExpression() { } /** - * 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 @@ public boolean hasConstructorExpression() { } /** - * 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 @@ public DeclaredQuery getQuery() { 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 @@ public String rewrite(QueryRewriteInformation rewriteInformation) { .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 cc2985fefc..719e838fe0 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.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,31 +150,39 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfigurat 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); } RepositoryQuery query = NamedQuery.lookupFrom(method, em, configuration.getSelector()); - return query != null // - ? query // - : NO_QUERY; + return query != null ? query : NO_QUERY; + } + + 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 @Nullable String getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) { + private static @Nullable String doGetCountQuery(JpaQueryMethod method, NamedQueries namedQueries, + EntityManager em) { if (StringUtils.hasText(method.getCountQuery())) { return method.getCountQuery(); @@ -205,20 +212,20 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfigurat * * @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 d6f2fc3d89..6c909446a5 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 @@ -26,9 +26,9 @@ import java.util.Optional; import java.util.Set; -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; @@ -279,6 +279,13 @@ public org.springframework.data.jpa.repository.query.Meta getQueryMetaAttributes 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. @@ -317,6 +324,25 @@ public String getRequiredAnnotatedQuery() throws IllegalStateException { 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. @@ -329,6 +355,19 @@ public String getRequiredAnnotatedQuery() throws IllegalStateException { 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. @@ -352,6 +391,17 @@ boolean isNativeQuery() { 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 83002d326f..ee805c7594 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 @@ -76,7 +76,7 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelecto 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(); @@ -90,14 +90,17 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelecto 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 4c2fefe23f..35045c5e25 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.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.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 fc34606f45..3776111b99 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 @@ static ParameterBinder createBinder(JpaParameters parameters, List getBindings(JpaParameters parameters) { 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) { + StructuredQuery query, QueryParameterSetterFactory... strategies) { List setters = new ArrayList<>(parameterBindings.size()); for (ParameterBinding parameterBinding : parameterBindings) { @@ -141,7 +141,7 @@ private static Iterable createSetters(List bindings; + private final boolean usesJdbcStyleParameters; private final boolean containsPageableInSpel; - private final QueryEnhancerFactory queryEnhancerFactory; - private final QueryEnhancer queryEnhancer; - private final boolean hasNamedParameters; - - /** - * 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"); + private final boolean hasNamedBindings; - 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()); + private ParametrizedQuery(DeclaredQuery query, List bindings, boolean usesJdbcStyleParameters, + boolean containsPageableInSpel) { + this.source = query; + this.bindings = bindings; + this.usesJdbcStyleParameters = usesJdbcStyleParameters; + this.containsPageableInSpel = containsPageableInSpel; + this.hasNamedBindings = containsNamedParameter(bindings); } - /** - * 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); + private static boolean containsNamedParameter(List bindings) { - this.queryEnhancerFactory = selector.select(source); - this.queryEnhancer = queryEnhancerFactory.create(this.bindableQuery); - parameterPostProcessor.accept(this.bindableQuery.getBindings()); - this.hasNamedParameters = containsNamedParameter(this.bindableQuery.getBindings()); + for (ParameterBinding parameterBinding : bindings) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { + return true; + } + } + return false; } /** - * internal copy constructor + * 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 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; - this.containsPageableInSpel = containsPageableInSpel; - } - - QueryEnhancer getQueryEnhancer() { - return queryEnhancer; - } - - /** - * Returns whether we have found some like bindings. + * @param declaredQuery the source query to parse. + * @return a parsed {@link ParametrizedQuery}. */ - boolean hasParameterBindings() { - return this.bindableQuery.hasBindings(); - } - - String getProjection() { - return this.queryEnhancer.getProjection(); + public static ParametrizedQuery parse(DeclaredQuery declaredQuery) { + return ParameterBindingParser.INSTANCE.parse(declaredQuery.getQueryString(), declaredQuery::rewrite, + parameterBindings -> {}); } @Override public String getQueryString() { - return bindableQuery.getQueryString(); + return source.getQueryString(); } @Override - public List getParameterBindings() { - return this.bindableQuery.getBindings(); + public boolean isNative() { + return source.isNative(); } - @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); - } - } - } - }); + boolean hasBindings() { + return !bindings.isEmpty(); } - @Override - public String applySorting(Sort sort) { - return queryEnhancer.applySorting(sort); + boolean hasNamedBindings() { + return this.hasNamedBindings; } - @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() { + boolean containsPageableInSpel() { return containsPageableInSpel; } - @Override - public DeclaredQuery getDeclaredQuery() { - return bindableQuery; + boolean usesJdbcStyleParameters() { + return usesJdbcStyleParameters; } - private static boolean containsNamedParameter(List bindings) { - for (ParameterBinding parameterBinding : bindings) { - if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { - return true; - } - } - return false; + List getBindings() { + return Collections.unmodifiableList(bindings); } /** - * Value object to track and allocate used parameter index labels in a query. + * Derive 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 */ - 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); + @Override + public ParametrizedQuery rewrite(String newQueryString) { - if (index == -1) { - throw new IllegalStateException( - "Unable to allocate a unique parameter label. All possible labels have been used."); - } + return ParameterBindingParser.INSTANCE.parse(newQueryString, source::rewrite, derivedBindings -> { - usedLabels.add(index); + // 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)) { - return index; - } + for (ParameterBinding binding : bindings) { - private int attemptAllocate(int attempts) { + Predicate identifier = binding::bindsTo; + Predicate notCompatible = Predicate.not(binding::isCompatibleWith); - for (int i = 0; i < attempts; i++) { - - if (usedLabels.contains(i + 1)) { - continue; + // replace incompatible bindings + if (derivedBindings.removeIf(it -> identifier.test(it) && notCompatible.test(it))) { + derivedBindings.add(binding); + } } - - return i + 1; } + }); + } - return -1; - } - - public boolean hasLabels() { - return !usedLabels.isEmpty(); - } + @Override + public String toString() { + return "ParametrizedQuery[" + source + ", " + bindings + ']'; } /** @@ -371,19 +211,24 @@ enum ParameterBindingParser { * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns * the cleaned up query. */ - BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(DeclaredQuery query) { + ParametrizedQuery 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(); @@ -429,28 +274,30 @@ BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(Dec parameterIndex = parameterLabels.allocate(); } - BindingIdentifier queryParameter; + ParameterBinding.BindingIdentifier queryParameter; if (parameterIndex != null) { - queryParameter = BindingIdentifier.of(parameterIndex); + queryParameter = ParameterBinding.BindingIdentifier.of(parameterIndex); } else if (parameterName != null) { - queryParameter = BindingIdentifier.of(parameterName); + 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); }; @@ -461,8 +308,7 @@ BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(Dec } replacement = targetBinding.hasName() ? ":" + targetBinding.getName() - : ((!usesJpaStyleParameters && jdbcStyle) ? "?" - : "?" + targetBinding.getPosition()); + : ((!usesJpaStyleParameters && jdbcStyle) ? "?" : "?" + targetBinding.getPosition()); String result; String substring = matcher.group(2); @@ -478,7 +324,9 @@ BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(Dec resultingQuery = result; } - return new BindableQuery(query, resultingQuery, bindings, jdbcStyle); + parameterBindingPostProcessor.accept(bindings); + return new ParametrizedQuery(declaredQueryFactory.apply(resultingQuery), bindings, jdbcStyle, + containsPageableInSpel); } private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, @@ -586,18 +434,17 @@ static ParameterBindingType of(String typeSource) { } } - - /** * 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 +458,22 @@ public ParameterBindings(List bindings, Consumer 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 +493,7 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, } } - BindingIdentifier syntheticIdentifier; + ParameterBinding.BindingIdentifier syntheticIdentifier; if (identifier.hasName() && methodArgument.hasName()) { int index = 0; @@ -654,9 +502,9 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, index++; newName = methodArgument.getName() + "_" + index; } - syntheticIdentifier = BindingIdentifier.of(newName); + syntheticIdentifier = ParameterBinding.BindingIdentifier.of(newName); } else { - syntheticIdentifier = BindingIdentifier.of(parameterLabels.allocate()); + syntheticIdentifier = ParameterBinding.BindingIdentifier.of(parameterLabels.allocate()); } ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier); @@ -670,7 +518,7 @@ private boolean existsBoundParameter(String key) { .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 +526,79 @@ public void register(ParameterBinding parameterBinding) { 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 528426f82f..145e94150a 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; /** @@ -31,6 +29,18 @@ */ 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 +49,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,58 +63,22 @@ 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. * @@ -116,7 +90,7 @@ default String createCountQueryFor() { /** * Interface to describe the information needed to rewrite a query. * - * @since 3.5 + * @since 4.0 */ interface QueryRewriteInformation { @@ -129,6 +103,7 @@ interface QueryRewriteInformation { * @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 07ed8642c3..ef7f141246 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 @@ -57,7 +57,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(StructuredQuery query) { + public QueryEnhancer create(QueryProvider query) { return new DefaultQueryEnhancer(query); } }, @@ -65,11 +65,11 @@ public QueryEnhancer create(StructuredQuery query) { 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 +81,33 @@ public QueryEnhancer create(StructuredQuery query) { HQL { @Override public boolean supports(DeclaredQuery query) { - return !query.isNativeQuery(); + return !query.isNative(); } @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.isNative(); } @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.isNative(); } @Override - public QueryEnhancer create(StructuredQuery query) { + public QueryEnhancer create(QueryProvider query) { return JpaQueryEnhancer.forJpql(query.getQueryString()); } } 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 26bdf4b5b2..984193b926 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 StructuredQuery}. * * @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 93268c6387..fd5f1da6ae 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 @@ * 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 QueryEnhancerFactory select(DeclaredQuery query) { } } + } 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 3a9d2af875..31f1fadc83 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 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, StructuredQuery parametrizedQuery); /** * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. @@ -180,7 +180,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, StructuredQuery parametrizedQuery) { if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) { return null; @@ -212,7 +212,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory { @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, StructuredQuery query) { if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) { return null; @@ -248,7 +248,7 @@ private static class BasicQueryParameterSetterFactory extends QueryParameterSett } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, StructuredQuery query) { Assert.notNull(binding, "Binding must not be null"); @@ -294,7 +294,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, StructuredQuery 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 6ba9f81ba6..98de7da6eb 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 { - - private final String sql; - - NativeQuery(String sql) { - this.sql = sql; - } +public interface QueryProvider { - @Override - public boolean isNativeQuery() { - return true; - } + /** + * Return the query string. + * + * @return the query string. + */ + String getQueryString(); - @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 1619dedb86..6c2919e91a 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,7 +445,7 @@ private static String toJpaDirection(Order order) { * * @param query must not be {@literal null}. * @return Might return {@literal null}. - * @deprecated use {@link IntrospectedQuery#getAlias()} instead. + * @deprecated use {@link StructuredQuery#getAlias()} instead. */ @Deprecated public static @Nullable String detectAlias(String query) { @@ -554,7 +554,7 @@ public static Query applyAndBind(String queryString, Iterable entities, E * * @param originalQuery must not be {@literal null} or empty. * @return Guaranteed to be not {@literal null}. - * @deprecated use {@link IntrospectedQuery#deriveCountQuery(String)} instead. + * @deprecated use {@link StructuredQuery#deriveCountQuery(String)} instead. */ @Deprecated public static String createCountQueryFor(String originalQuery) { @@ -568,7 +568,7 @@ public static String createCountQueryFor(String originalQuery) { * @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 use {@link StructuredQuery#deriveCountQuery(String)} instead. */ @Deprecated public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) { 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 b913061ad6..b042318b13 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 @@ 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 index 2ebfcb0549..9bb4aea060 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2025 the original author or authors. + * Copyright 2018-2024 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. @@ -15,10 +15,45 @@ */ package org.springframework.data.jpa.repository.query; +import java.util.List; + /** - * @author Christoph Strobl + * 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 4.0 + * @see EntityQuery + * @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector) + * @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration) */ -public interface StructuredQuery { +interface StructuredQuery extends QueryProvider { + + /** + * @return whether the underlying query has at least one parameter. + */ + boolean hasParameterBindings(); + + /** + * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or + * name. + * + * @return Whether the query uses JDBC style parameters. + * @since 2.0.6 + */ + boolean usesJdbcStyleParameters(); + + /** + * @return whether the underlying query has at least one named parameter. + */ + boolean hasNamedParameter(); + + /** + * @return the registered {@link ParameterBinding}s. + */ + List getParameterBindings(); - 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 b6c93b5604..487a7b11f8 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.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 @@ * @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 @@ private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEnti 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 @@ private static boolean containsExpression(String query) { 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 204471b6d9..3d77980fb6 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 @@ void createsNormalQueryForJpaManagedReturnTypes() throws Exception { 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 adc489cc98..953203134f 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 @@ public EntityManager get() { } @Override - protected String applySorting(CachableQuery query) { + protected QueryProvider applySorting(CachableQuery query) { captureInvocation("applySorting", query); @@ -158,12 +158,13 @@ protected String applySorting(CachableQuery query) { } @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 3c18eda1fb..ea5082d9f1 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.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 @@ -43,13 +42,13 @@ * @author Mark Paluch * @author Aleksei Elin */ -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); @@ -66,8 +65,8 @@ void doesNotConsiderPlainLikeABinding() { @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()) @@ -90,7 +89,7 @@ void detectsPositionalLikeBindings() { @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(); @@ -116,7 +115,8 @@ void detectsAnonymousLikeBindings() { @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"); @@ -133,7 +133,7 @@ void detectsNamedLikeBindings() { @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); @@ -164,7 +164,7 @@ void rewritesNamedLikeToUniqueParametersIfNecessary() { @Test // GH-3784 void rewritesNamedLikeToUniqueParametersRetainingCountQuery() { - DeclaredQuery query = new StringQuery( + StructuredQuery 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); @@ -197,7 +197,7 @@ void rewritesNamedLikeToUniqueParametersRetainingCountQuery() { @Test // GH-3784 void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() { - DeclaredQuery query = new StringQuery( + StructuredQuery query = new TestEntityQuery( "select u from User u where u.firstname like %:#{firstname} or u.firstname like :#{firstname}%", false) .deriveCountQuery(null); @@ -224,7 +224,7 @@ void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() { @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(); @@ -238,7 +238,7 @@ void rewritesPositionalLikeToUniqueParametersIfNecessary() { @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); @@ -246,7 +246,8 @@ void reusesNamedLikeBindingsWherePossible() { 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()) @@ -256,7 +257,7 @@ void reusesNamedLikeBindingsWherePossible() { @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); @@ -264,7 +265,7 @@ void reusesPositionalLikeBindingsWherePossible() { 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"); @@ -273,7 +274,7 @@ void reusesPositionalLikeBindingsWherePossible() { @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); @@ -295,8 +296,8 @@ void shouldRewritePositionalBindingsWithParameterReuse() { @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) @@ -309,8 +310,8 @@ void createsDistinctBindingsForIndexedSpel() { @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) @@ -322,7 +323,7 @@ void createsDistinctBindingsForNamedSpel() { 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); @@ -337,7 +338,7 @@ void detectsNamedInParameterBindings() { 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); @@ -354,7 +355,7 @@ void detectsMultipleNamedInParameterBindings() { 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); + StructuredQuery 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)"); @@ -375,7 +376,7 @@ void deriveCountQueryWithNamedInRetainsOrigin() { 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); + StructuredQuery 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)"); @@ -396,7 +397,7 @@ void deriveCountQueryWithPositionalInRetainsOrigin() { 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); @@ -410,7 +411,7 @@ void detectsPositionalInParameterBindings() { @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(); @@ -423,7 +424,7 @@ void allowsReuseOfParameterWithInAndRegularBinding() { 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); @@ -442,7 +443,7 @@ void allowsReuseOfParameterWithInAndRegularBinding() { 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"); } @@ -451,7 +452,7 @@ void detectsPositionalInParameterBindingsAndExpressions() { 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"); } @@ -459,17 +460,17 @@ void detectsPositionalInParameterBindingsAndExpressionsWithReuse() { @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); + StructuredQuery 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); @@ -484,17 +485,17 @@ void countQueryDerivationRetainsNamedExpressionParameters() { @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); + StructuredQuery 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); @@ -510,7 +511,7 @@ void countQueryDerivationRetainsIndexedExpressionParameters() { 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); @@ -526,13 +527,13 @@ void detectsMultiplePositionalInParameterBindings() { @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); @@ -543,8 +544,10 @@ void treatsGreaterThanBindingAsSimpleBinding() { @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(); @@ -559,7 +562,8 @@ void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() { @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(); @@ -570,7 +574,7 @@ void detectsInBindingWithParentheses() { @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(); @@ -581,7 +585,7 @@ void detectsInBindingWithSpecialFrenchCharactersInParentheses() { @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(); @@ -592,7 +596,7 @@ void detectsInBindingWithSpecialCharactersInParentheses() { @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(); @@ -603,7 +607,7 @@ void detectsInBindingWithSpecialAsianCharactersInParentheses() { @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(); @@ -614,7 +618,7 @@ void detectsInBindingWithSpecialCharactersAndWordCharactersMixedInParentheses() @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(); @@ -625,7 +629,7 @@ void shouldReplaceAllNamedExpressionParametersWithInClause() { @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(); @@ -636,8 +640,8 @@ void shouldReplaceExpressionWithLikeParameters() { @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"); @@ -653,12 +657,11 @@ void shouldReplaceAllPositionExpressionParametersWithInClause() { @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(); } /** @@ -669,14 +672,16 @@ void detectsConstructorExpressions() { 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(); @@ -703,7 +708,7 @@ void getProjection() { 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); } @@ -727,7 +732,7 @@ void getAlias() { 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); } @@ -780,7 +785,7 @@ void ignoresQuotedNamedParameterLookAlike() { 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(); @@ -800,16 +805,18 @@ void failOnMixedBindingsWithoutIndex() { 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( // @@ -820,7 +827,7 @@ void makesUsageOfJdbcStyleParameterAvailable() { for (String testQuery : testQueries) { - assertThat(new StringQuery(testQuery, false) // + assertThat(new TestEntityQuery(testQuery, false) // .usesJdbcStyleParameters()) // .describedAs(testQuery) // .describedAs(testQuery) // @@ -832,7 +839,7 @@ void makesUsageOfJdbcStyleParameterAvailable() { 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(); @@ -852,7 +859,7 @@ void isNotDefaultProjection() { "select a, b from C"); for (String queryString : queriesWithoutDefaultProjection) { - assertThat(new StringQuery(queryString, true).isDefaultProjection()) // + assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) // .describedAs(queryString) // .isFalse(); } @@ -869,7 +876,7 @@ void isNotDefaultProjection() { ); for (String queryString : queriesWithDefaultProjection) { - assertThat(new StringQuery(queryString, true).isDefaultProjection()) // + assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) // .describedAs(queryString) // .isTrue(); } @@ -879,7 +886,7 @@ void isNotDefaultProjection() { 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(); @@ -891,7 +898,7 @@ void questionMarkInStringLiteralWithParameters() { 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) // @@ -902,7 +909,7 @@ void usingPipesWithNamedParameter() { 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) // @@ -911,9 +918,8 @@ void usingGreaterThanWithNamedParameter() { 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) // @@ -926,7 +932,8 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) { DeclaredQuery source = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); - BindableQuery bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); + ParametrizedQuery bindableQuery = ParametrizedQuery.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 9a5c9ff30f..7dd6dd757c 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.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 @@ void shouldApplySorting() { 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 @@ void shouldApplySorting() { @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 5303378b84..dbe4d45a9f 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 61436aae55..8f93859699 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.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 @@ void applySortingAccountsForNewlinesInSubselect() { 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 @@ private void assertCountQuery(String originalQuery, String countQuery) { } 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 916db5e06a..f25e9fc2ee 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 d9634ea91c..cd2c3987fc 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.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 @@ void applySortingAccountsForNewlinesInSubselect() { 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 @@ private void assertCountQuery(String originalQuery, String countQuery) { } 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 52787f910f..4a0be8de58 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 @@ -25,6 +25,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.data.domain.Sort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; /** * TCK Tests for {@link JSqlParserQueryEnhancer}. @@ -46,7 +48,8 @@ void shouldApplySorting() { 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"); } @@ -62,7 +65,7 @@ void countQueriesShouldConsiderPrimaryTableAlias() { 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"); } @@ -82,16 +85,16 @@ void setOperationListWorks() { + "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(); @@ -105,16 +108,16 @@ void complexSetOperationListWorks() { + "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(); @@ -132,16 +135,16 @@ void deeplyNestedcomplexSetOperationListWorks() { + "\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(); @@ -152,16 +155,15 @@ void valuesStatementsWorks() { 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(); @@ -173,18 +175,18 @@ void withStatementsWorks() { 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()).isEqualToIgnoringCase( + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase( "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(); @@ -196,18 +198,18 @@ void multipleWithStatementsWorks() { 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()).isEqualToIgnoringCase( + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase( "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(); @@ -216,15 +218,15 @@ void multipleWithStatementsWorks() { @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(); @@ -232,15 +234,14 @@ void truncateStatementShouldWork() { @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(); @@ -257,4 +258,9 @@ static Stream mergeStatementWorksSource() { null)); } + 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 32f9e965a9..44256fe4c9 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 1a38f729e2..39ed9b6d9d 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.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 @@ void applySortingAccountsForNewlinesInSubselect() { 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 @@ private void assertCountQuery(String originalQuery, String countQuery) { } 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 fa44d2ca11..c17cc49f94 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.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 @@ void shouldApplySorting() { 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 edcaf0e4ea..f765860a27 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 @@ 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 @@ void identificationOfParameters() { 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 aaccc4cad4..2f52341214 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 @@ void createsParsingImplementationForNonNativeQuery() { @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 7a0f4e1783..98e19b6cb7 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 @@ static Stream jpqlCountQueries() { 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 @@ static Stream nativeQueriesWithVariables() { // 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 0e5f44cd8b..da113f567b 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 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.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 @@ * @author Diego Krupitza * @author Geoffrey Deremetz * @author Krzysztof Krason + * @author Mark Paluch */ class QueryEnhancerUnitTests { @@ -78,7 +81,7 @@ void allowsShortJpaSyntax() { @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 @@ void detectsAliasWithUCorrectly(IntrospectedQuery query, String alias) { 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 @@ void allowsFullyQualifiedEntityNamesInQuery() { @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 @@ void appliesIgnoreCaseOrderingCorrectly() { 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 @@ void appendsIgnoreCaseOrderingCorrectly() { 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 @@ void projectsCountQueriesForQueriesWithSubSelects() { @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 @@ void doesNotPrefixSortsIfFunction() { 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 @@ void findsExistingOrderByIndependentOfCase() { @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 @@ void createCountQueryFromDeleteQuery() { @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 @@ void createCountQueryFromTheGivenCountProjection() { @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 @@ void detectsAliasInQueryContainingLineBreaks() { @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 @@ void detectsConstructorExpressionInDistinctQuery() { @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 @@ void detectsComplexConstructorExpression() { @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 @@ void detectsConstructorExpressionWithLineBreaks() { @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 @@ void discoversAliasWithComplexFunction() { @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 @@ void detectsAliasWithGroupAndOrderBy() { @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 @@ void appliesSortCorrectlyForFieldAliases() { @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 @@ void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() { @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 @@ void appliesSortCorrectlyForFunctionAliases() { @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 @@ void appliesSortCorrectlyForSimpleField() { @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 @@ void detectsAliasWithGroupAndOrderByWithLineBreaks() { @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 @@ void findProjectionClauseWithDistinct(IntrospectedQuery query, String expected) 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 @@ void findProjectionClauseWithSubselectNative() { // 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 @@ void correctApplySortOnComplexNestedFunctionQuery() { + " 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 @@ void modifyingQueriesAreDetectedCorrectly() { 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 @@ void insertStatementIsProcessedSameAsDefault(String insertQuery) { 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 @@ public static Stream detectsJoinAliasesCorrectlySource() { } 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(DefaultEntityQuery originalQuery, String countQuery) { + assertThat(getEnhancer(originalQuery).createCountQueryFor(null)).isEqualToIgnoringCase(countQuery); } - private static void assertCountQuery(StringQuery originalQuery, String countQuery) { - assertThat(getEnhancer(originalQuery).createCountQueryFor()).isEqualToIgnoringCase(countQuery); + private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) { + return new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); } - private static QueryEnhancer getEnhancer(IntrospectedQuery query) { - return QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query.getDeclaredQuery()); + 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 34d3ab2397..d4fb9a761d 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 @@ void before() { @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 @@ void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() { 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 @@ void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() { 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 5887eab53b..188166d3bd 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 @@ void prefersDeclaredCountQueryOverCreatingOne() throws Exception { 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 @@ void doesNotApplyPaginationToCountQuery() throws Exception { 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 @@ void discoversNativeQuery() throws Exception { 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 @@ void discoversNativeQueryFromNativeQueryInterface() throws Exception { 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 @@ void resolvesExpressionInCountQuery() throws Exception { 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 @@ private AbstractJpaQuery createJpaQuery(Method method) { 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 a235543017..6581b628f7 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.parser.Part.Type; /** - * Unit tests for {@link ExpressionBasedStringQuery}. + * Unit tests for {@link TemplatedQuery}. * * @author Thomas Darimont * @author Oliver Gierke @@ -45,7 +45,7 @@ */ @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 @@ void setUp() { 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 @@ void renderAliasInExpressionQueryCorrectly() { @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 @@ void shouldDetectBindParameterCountCorrectly() { @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 @@ void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() { @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 @@ void namedExpressionsShouldCreateLikeBindings() { @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 @@ void indexedExpressionsShouldCreateLikeBindings() { @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 @@ void doesTemplatingWhenEntityNameSpelIsPresent() { @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 @@ void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { @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 6a8f3cce03..f260302121 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 { - - private final String jpql; - - JpqlQuery(String jpql) { - this.jpql = jpql; - } +class TestEntityQuery extends DefaultEntityQuery { - @Override - public boolean isNativeQuery() { - return false; - } + /** + * Creates a new {@link DefaultEntityQuery} from the given JPQL query. + * + * @param query must not be {@literal null} or empty. + */ + TestEntityQuery(String query, boolean isNative) { - @Override - public String getQueryString() { - return jpql; - } + super(ParametrizedQuery.parse(isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query)), + QueryEnhancerSelector.DEFAULT_SELECTOR + .select(isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query))); + } } From 3c6179246092b3dbfd9da565aab9e214bb93b05b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 18 Mar 2025 11:17:08 +0100 Subject: [PATCH 7/7] Documentation --- .../repository/JpaSpecificationExecutor.java | 11 - .../data/jpa/repository/NativeQuery.java | 1 + .../data/jpa/repository/Query.java | 1 + .../query/AbstractStringBasedJpaQuery.java | 6 +- .../repository/query/DefaultEntityQuery.java | 27 +- .../query/EmptyIntrospectedQuery.java | 4 +- .../jpa/repository/query/EntityQuery.java | 8 +- .../repository/query/JpaQueryEnhancer.java | 6 +- .../query/ParameterBinderFactory.java | 6 +- .../repository/query/ParametrizedQuery.java | 589 +---------------- .../repository/query/PreprocessedQuery.java | 625 ++++++++++++++++++ .../jpa/repository/query/QueryEnhancer.java | 5 +- .../query/QueryEnhancerFactory.java | 2 +- .../query/QueryParameterSetterFactory.java | 10 +- .../data/jpa/repository/query/QueryUtils.java | 12 +- .../jpa/repository/query/StructuredQuery.java | 59 -- .../query/DefaultEntityQueryUnitTests.java | 14 +- .../jpa/repository/query/TestEntityQuery.java | 2 +- .../modules/ROOT/pages/jpa/query-methods.adoc | 209 +++--- 19 files changed, 813 insertions(+), 784 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java 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 ffd6f55529..536ff5bca2 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 @@ default List findAll(PredicateSpecification spec) { * 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 d10c90b68c..d12036c74b 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 @@ * 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 12ff41bb71..4405d29bbb 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 @@ * @since 3.0 */ Class queryRewriter() default QueryRewriter.IdentityQueryRewriter.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 3510dbf9c5..f8f26e48cc 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 @@ -49,7 +49,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final EntityQuery query; - private final Lazy countQuery; + private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; @@ -151,7 +151,7 @@ protected ParameterBinder createBinder() { return createBinder(query); } - protected ParameterBinder createBinder(StructuredQuery query) { + protected ParameterBinder createBinder(ParametrizedQuery query) { return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, valueExpressionDelegate, valueExpressionContextProvider); } @@ -182,7 +182,7 @@ public EntityQuery getQuery() { /** * @return the countQuery */ - public StructuredQuery getCountQuery() { + public ParametrizedQuery getCountQuery() { return countQuery.get(); } 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 index 5d8807654c..bde36d1535 100644 --- 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 @@ -22,7 +22,7 @@ /** * Encapsulation of a JPA query string, typically returning entities or DTOs. Provides access to parameter bindings. *

        - * The internal {@link ParametrizedQuery query string} is cleaned from decorated parameters like {@literal %:lastname%} + * 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. * @@ -38,10 +38,10 @@ */ class DefaultEntityQuery implements EntityQuery, DeclaredQuery { - private final ParametrizedQuery query; + private final PreprocessedQuery query; private final QueryEnhancer queryEnhancer; - DefaultEntityQuery(ParametrizedQuery query, QueryEnhancerFactory queryEnhancerFactory) { + DefaultEntityQuery(PreprocessedQuery query, QueryEnhancerFactory queryEnhancerFactory) { this.query = query; this.queryEnhancer = queryEnhancerFactory.create(query); } @@ -89,22 +89,23 @@ public boolean isDefaultProjection() { return queryEnhancer.getProjection().equalsIgnoreCase(getAlias()); } + @Nullable + String getAlias() { + return queryEnhancer.detectAlias(); + } + @Override public boolean usesPaging() { return query.containsPageableInSpel(); } - public @Nullable String getAlias() { - return queryEnhancer.detectAlias(); - } - String getProjection() { return this.queryEnhancer.getProjection(); } @Override - public StructuredQuery deriveCountQuery(@Nullable String countQueryProjection) { - return new SimpleStructuredQuery(this.query.rewrite(queryEnhancer.createCountQueryFor(countQueryProjection))); + public ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection) { + return new SimpleParametrizedQuery(this.query.rewrite(queryEnhancer.createCountQueryFor(countQueryProjection))); } @Override @@ -118,13 +119,13 @@ public String toString() { } /** - * Simple {@link StructuredQuery} variant forwarding to {@link ParametrizedQuery}. + * Simple {@link ParametrizedQuery} variant forwarding to {@link PreprocessedQuery}. */ - static class SimpleStructuredQuery implements StructuredQuery { + static class SimpleParametrizedQuery implements ParametrizedQuery { - private final ParametrizedQuery query; + private final PreprocessedQuery query; - SimpleStructuredQuery(ParametrizedQuery query) { + SimpleParametrizedQuery(PreprocessedQuery query) { this.query = 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 7ab38bbe06..a0ef2363b6 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 @@ -21,7 +21,7 @@ import org.jspecify.annotations.Nullable; /** - * NULL-Object pattern implementation for {@link StructuredQuery}. + * NULL-Object pattern implementation for {@link ParametrizedQuery}. * * @author Jens Schauder * @author Mark Paluch @@ -73,7 +73,7 @@ public String getQueryString() { } @Override - public StructuredQuery deriveCountQuery(@Nullable String countQueryProjection) { + public ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection) { return INSTANCE; } 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 45e6ba5021..b28fa9f10d 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 @@ -18,7 +18,7 @@ import org.jspecify.annotations.Nullable; /** - * An extension to {@link StructuredQuery} exposing query information about its inner structure such as whether + * 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 @@ -28,7 +28,7 @@ * @author Diego Krupitza * @since 4.0 */ -interface EntityQuery extends StructuredQuery { +interface EntityQuery extends ParametrizedQuery { /** * Create a new {@link EntityQuery} given {@link DeclaredQuery} and {@link QueryEnhancerSelector}. @@ -39,7 +39,7 @@ interface EntityQuery extends StructuredQuery { */ static EntityQuery create(DeclaredQuery query, QueryEnhancerSelector selector) { - ParametrizedQuery preparsed = ParametrizedQuery.parse(query); + PreprocessedQuery preparsed = PreprocessedQuery.parse(query); QueryEnhancerFactory enhancerFactory = selector.select(preparsed); return new DefaultEntityQuery(preparsed, enhancerFactory); @@ -73,7 +73,7 @@ default boolean usesPaging() { * @param countQueryProjection an optional return type for the query. * @return a new {@literal IntrospectedQuery} instance. */ - StructuredQuery deriveCountQuery(@Nullable String countQueryProjection); + ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection); /** * Rewrite the query using the given 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 b9b8a72b43..04d134c0ad 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 @@ -140,7 +140,7 @@ static void configureParser(String query, String grammar, Lexer lexer, Parser pa } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link StructuredQuery} 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. @@ -150,7 +150,7 @@ public static JpaQueryEnhancer forJpql(String query) { } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link StructuredQuery} 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. @@ -160,7 +160,7 @@ public static JpaQueryEnhancer forHql(String query) { } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link StructuredQuery} 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. 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 3776111b99..00aef26195 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 @@ -84,7 +84,7 @@ static ParameterBinder createBinder(JpaParameters parameters, List createSetters(List createSetters(List parameterBindings, - StructuredQuery query, QueryParameterSetterFactory... strategies) { + ParametrizedQuery query, QueryParameterSetterFactory... strategies) { List setters = new ArrayList<>(parameterBindings.size()); for (ParameterBinding parameterBinding : parameterBindings) { @@ -141,7 +141,7 @@ private static Iterable createSetters(List + * Structured queries can be either created from {@link EntityQuery} introspection or through + * {@link EntityQuery#deriveCountQuery(String) count query derivation}. * - * @author Christoph Strobl - * @author Mark Paluch + * @author Jens Schauder + * @author Diego Krupitza * @since 4.0 + * @see EntityQuery + * @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector) + * @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration) */ -final class ParametrizedQuery implements DeclaredQuery { - - private final DeclaredQuery source; - private final List bindings; - private final boolean usesJdbcStyleParameters; - private final boolean containsPageableInSpel; - private final boolean hasNamedBindings; - - private ParametrizedQuery(DeclaredQuery query, List bindings, boolean usesJdbcStyleParameters, - boolean containsPageableInSpel) { - this.source = query; - this.bindings = bindings; - this.usesJdbcStyleParameters = usesJdbcStyleParameters; - this.containsPageableInSpel = containsPageableInSpel; - this.hasNamedBindings = containsNamedParameter(bindings); - } - - private static boolean containsNamedParameter(List bindings) { - - for (ParameterBinding parameterBinding : bindings) { - if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { - return true; - } - } - return false; - } +interface ParametrizedQuery extends QueryProvider { /** - * 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 ParametrizedQuery}. + * @return whether the underlying query has at least one parameter. */ - public static ParametrizedQuery parse(DeclaredQuery declaredQuery) { - return ParameterBindingParser.INSTANCE.parse(declaredQuery.getQueryString(), declaredQuery::rewrite, - parameterBindings -> {}); - } - - @Override - public String getQueryString() { - return source.getQueryString(); - } - - @Override - public boolean isNative() { - return source.isNative(); - } - - boolean hasBindings() { - return !bindings.isEmpty(); - } - - boolean hasNamedBindings() { - return this.hasNamedBindings; - } - - boolean containsPageableInSpel() { - return containsPageableInSpel; - } - - boolean usesJdbcStyleParameters() { - return usesJdbcStyleParameters; - } - - List getBindings() { - return Collections.unmodifiableList(bindings); - } + boolean hasParameterBindings(); /** - * Derive 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. + * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or + * name. * - * @return + * @return Whether the query uses JDBC style parameters. + * @since 2.0.6 */ - @Override - public ParametrizedQuery 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); - } - } - } - }); - } - - @Override - public String toString() { - return "ParametrizedQuery[" + source + ", " + bindings + ']'; - } + boolean usesJdbcStyleParameters(); /** - * A parser that extracts the parameter bindings from a given query string. - * - * @author Thomas Darimont + * @return whether the underlying query has at least one named parameter. */ - enum ParameterBindingParser { - - INSTANCE; - - private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__"; - public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))"; - // .....................................................................^ not followed by a hash or a letter. - // .................................................................^ zero or more digits. - // .............................................................^ start with a question mark. - private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER); - private static final Pattern PARAMETER_BINDING_PATTERN; - private static final Pattern JDBC_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?(?!\\d)"); // no \ and [no digit] - private static final Pattern NUMBERED_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?\\d"); // no \ and [digit] - 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"; - private static final int INDEXED_PARAMETER_GROUP = 4; - private static final int NAMED_PARAMETER_GROUP = 6; - private static final int COMPARISION_TYPE_GROUP = 1; - - static { - - List keywords = new ArrayList<>(); - - for (ParameterBindingType type : ParameterBindingType.values()) { - if (type.getKeyword() != null) { - keywords.add(type.getKeyword()); - } - } - - StringBuilder builder = new StringBuilder(); - builder.append("("); - builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords - builder.append(")?"); - builder.append("(?: )?"); // some whitespace - builder.append("\\(?"); // optional braces around parameters - builder.append("("); - builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index - builder.append("|"); // or - - // named parameter and the parameter name - builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?"); - - builder.append(")"); - builder.append("\\)?"); // optional braces around parameters - - PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); - } - - /** - * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns - * the cleaned up query. - */ - ParametrizedQuery 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.contains("?#{")) { - parametersShouldBeAccessedByIndex = true; - } - - ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query, - parametersShouldBeAccessedByIndex, parameterLabels); - - String resultingQuery = parsedQuery.getQueryString(); - Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery); - - ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings)); - int currentIndex = 0; - - boolean usesJpaStyleParameters = false; - - while (matcher.find()) { - - if (parsedQuery.isQuoted(matcher.start())) { - continue; - } - - String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP); - String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP); - Integer parameterIndex = getParameterIndex(parameterIndexString); - - String match = matcher.group(0); - if (JDBC_STYLE_PARAM.matcher(match).find()) { - jdbcStyle = true; - } - - if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) { - usesJpaStyleParameters = true; - } - - if (usesJpaStyleParameters && jdbcStyle) { - throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported"); - } - - String typeSource = matcher.group(COMPARISION_TYPE_GROUP); - Assert.isTrue(parameterIndexString != null || parameterName != null, - () -> String.format("We need either a name or an index; Offending query string: %s", query)); - ValueExpression expression = parsedQuery - .getParameter(parameterName == null ? parameterIndexString : parameterName); - String replacement = null; - - // this only happens for JDBC-style parameters. - if ("".equals(parameterIndexString)) { - parameterIndex = parameterLabels.allocate(); - } - - ParameterBinding.BindingIdentifier queryParameter; - if (parameterIndex != null) { - queryParameter = ParameterBinding.BindingIdentifier.of(parameterIndex); - } else if (parameterName != null) { - queryParameter = ParameterBinding.BindingIdentifier.of(parameterName); - } else { - throw new IllegalStateException("No bindable expression found"); - } - ParameterBinding.ParameterOrigin origin = ObjectUtils.isEmpty(expression) - ? ParameterBinding.ParameterOrigin.ofParameter(parameterName, parameterIndex) - : ParameterBinding.ParameterOrigin.ofExpression(expression); - - ParameterBinding.BindingIdentifier targetBinding = queryParameter; - Function bindingFactory = switch (ParameterBindingType - .of(typeSource)) { - case LIKE -> { - - Part.Type likeType = ParameterBinding.LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); - yield (identifier) -> new ParameterBinding.LikeParameterBinding(identifier, origin, likeType); - } - 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 { - targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory, parameterLabels); - } - - replacement = targetBinding.hasName() ? ":" + targetBinding.getName() - : ((!usesJpaStyleParameters && jdbcStyle) ? "?" : "?" + targetBinding.getPosition()); - String result; - String substring = matcher.group(2); - - int index = resultingQuery.indexOf(substring, currentIndex); - if (index < 0) { - result = resultingQuery; - } else { - currentIndex = index + replacement.length(); - result = resultingQuery.substring(0, index) + replacement - + resultingQuery.substring(index + substring.length()); - } - - resultingQuery = result; - } - - parameterBindingPostProcessor.accept(bindings); - return new ParametrizedQuery(declaredQueryFactory.apply(resultingQuery), bindings, jdbcStyle, - containsPageableInSpel); - } - - private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, - boolean parametersShouldBeAccessedByIndex, IndexedParameterLabels parameterLabels) { - - /* - * If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to - * not mix-up with the actual parameter indices. - */ - BiFunction indexToParameterName = parametersShouldBeAccessedByIndex - ? (index, expression) -> String.valueOf(parameterLabels.allocate()) - : (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1); - - String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":"; - - BiFunction parameterNameToReplacement = (prefix, name) -> fixedPrefix + name; - ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(ValueExpressionParser.create(), - indexToParameterName, parameterNameToReplacement); - - return rewriter.parse(queryWithSpel); - } - - private static @Nullable Integer getParameterIndex(@Nullable String parameterIndexString) { - - if (parameterIndexString == null || parameterIndexString.isEmpty()) { - return null; - } - return Integer.valueOf(parameterIndexString); - } - - private static Set findParameterIndices(String query) { - - Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query); - Set usedParameterIndices = new TreeSet<>(); - - while (parameterIndexMatcher.find()) { - - String parameterIndexString = parameterIndexMatcher.group(1); - Integer parameterIndex = getParameterIndex(parameterIndexString); - if (parameterIndex != null) { - usedParameterIndices.add(parameterIndex); - } - } - - return usedParameterIndices; - } - - private static void checkAndRegister(ParameterBinding binding, List bindings) { - - bindings.stream() // - .filter(it -> it.bindsTo(binding)) // - .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding))); - - if (!bindings.contains(binding)) { - bindings.add(binding); - } - } - - /** - * An enum for the different types of bindings. - * - * @author Thomas Darimont - * @author Oliver Gierke - */ - private enum ParameterBindingType { - - // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace - // character, while = does not. - LIKE("like "), IN("in "), AS_IS(null); - - private final @Nullable String keyword; - - ParameterBindingType(@Nullable String keyword) { - this.keyword = keyword; - } - - /** - * Returns the keyword that will trigger the binding type or {@literal null} if the type is not triggered by a - * keyword. - * - * @return the keyword - */ - public @Nullable String getKeyword() { - return keyword; - } - - /** - * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal #AS_IS} in - * case no other {@link ParameterBindingType} could be found. - */ - static ParameterBindingType of(String typeSource) { - - if (!StringUtils.hasText(typeSource)) { - return AS_IS; - } - - for (ParameterBindingType type : values()) { - if (type.name().equalsIgnoreCase(typeSource.trim())) { - return type; - } - } - - throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s", typeSource)); - } - } - } - - /** - * 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 ParameterBinding.LikeParameterBinding#prepare(Object) LIKE - * rewrite}. - * - * @author Mark Paluch - * @since 3.1.2 - */ - private static class ParameterBindings { - - private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); - - private final Consumer registration; - - public ParameterBindings(List bindings, Consumer registration) { - - for (ParameterBinding binding : bindings) { - this.methodArgumentToLikeBindings.put(binding.getIdentifier(), new ArrayList<>(List.of(binding))); - } - - this.registration = registration; - } - - /** - * @param identifier - * @return whether the identifier is already bound. - */ - public boolean isBound(ParameterBinding.BindingIdentifier identifier) { - return !getBindings(identifier).isEmpty(); - } - - ParameterBinding.BindingIdentifier register(ParameterBinding.BindingIdentifier identifier, - ParameterBinding.ParameterOrigin origin, - Function bindingFactory, - IndexedParameterLabels parameterLabels) { - - Assert.isInstanceOf(ParameterBinding.MethodInvocationArgument.class, origin); - - ParameterBinding.BindingIdentifier methodArgument = ((ParameterBinding.MethodInvocationArgument) origin) - .identifier(); - List bindingsForOrigin = getBindings(methodArgument); - - if (!isBound(identifier)) { - - ParameterBinding binding = bindingFactory.apply(identifier); - registration.accept(binding); - bindingsForOrigin.add(binding); - return binding.getIdentifier(); - } - - ParameterBinding binding = bindingFactory.apply(identifier); - - for (ParameterBinding existing : bindingsForOrigin) { - - if (existing.isCompatibleWith(binding)) { - return existing.getIdentifier(); - } - } - - ParameterBinding.BindingIdentifier syntheticIdentifier; - if (identifier.hasName() && methodArgument.hasName()) { - - int index = 0; - String newName = methodArgument.getName(); - while (existsBoundParameter(newName)) { - index++; - newName = methodArgument.getName() + "_" + index; - } - syntheticIdentifier = ParameterBinding.BindingIdentifier.of(newName); - } else { - syntheticIdentifier = ParameterBinding.BindingIdentifier.of(parameterLabels.allocate()); - } - - ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier); - registration.accept(newBinding); - bindingsForOrigin.add(newBinding); - return newBinding.getIdentifier(); - } - - private boolean existsBoundParameter(String key) { - return methodArgumentToLikeBindings.values().stream().flatMap(Collection::stream) - .anyMatch(it -> key.equals(it.getName())); - } - - private List getBindings(ParameterBinding.BindingIdentifier identifier) { - return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>()); - } - - public void register(ParameterBinding parameterBinding) { - registration.accept(parameterBinding); - } - } + boolean hasNamedParameter(); /** - * Value object to track and allocate used parameter index labels in a query. + * @return the registered {@link ParameterBinding}s. */ - 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(); - } - } + List getParameterBindings(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java new file mode 100644 index 0000000000..0c5061b529 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java @@ -0,0 +1,625 @@ +/* + * 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 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; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jspecify.annotations.Nullable; + +import org.springframework.data.expression.ValueExpression; +import org.springframework.data.expression.ValueExpressionParser; +import org.springframework.data.repository.query.ValueExpressionQueryRewriter; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * 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 Christoph Strobl + * @author Mark Paluch + * @since 4.0 + */ +final class PreprocessedQuery implements DeclaredQuery { + + private final DeclaredQuery source; + private final List bindings; + private final boolean usesJdbcStyleParameters; + private final boolean containsPageableInSpel; + private final boolean hasNamedBindings; + + private PreprocessedQuery(DeclaredQuery query, List bindings, boolean usesJdbcStyleParameters, + boolean containsPageableInSpel) { + this.source = query; + this.bindings = bindings; + this.usesJdbcStyleParameters = usesJdbcStyleParameters; + this.containsPageableInSpel = containsPageableInSpel; + this.hasNamedBindings = containsNamedParameter(bindings); + } + + private static boolean containsNamedParameter(List bindings) { + + for (ParameterBinding parameterBinding : bindings) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin() + .isMethodArgument()) { + return true; + } + } + return false; + } + + /** + * 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}. + */ + public static PreprocessedQuery parse(DeclaredQuery declaredQuery) { + return ParameterBindingParser.INSTANCE.parse(declaredQuery.getQueryString(), declaredQuery::rewrite, + parameterBindings -> { + }); + } + + @Override + public String getQueryString() { + return source.getQueryString(); + } + + @Override + public boolean isNative() { + return source.isNative(); + } + + boolean hasBindings() { + return !bindings.isEmpty(); + } + + boolean hasNamedBindings() { + return this.hasNamedBindings; + } + + boolean containsPageableInSpel() { + return containsPageableInSpel; + } + + 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); + } + } + } + }); + } + + @Override + public String toString() { + return "ParametrizedQuery[" + source + ", " + bindings + ']'; + } + + /** + * A parser that extracts the parameter bindings from a given query string. + * + * @author Thomas Darimont + */ + enum ParameterBindingParser { + + INSTANCE; + + private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__"; + public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))"; + // .....................................................................^ not followed by a hash or a letter. + // .................................................................^ zero or more digits. + // .............................................................^ start with a question mark. + private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER); + private static final Pattern PARAMETER_BINDING_PATTERN; + private static final Pattern JDBC_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?(?!\\d)"); // no \ and [no digit] + private static final Pattern NUMBERED_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?\\d"); // no \ and [digit] + 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"; + private static final int INDEXED_PARAMETER_GROUP = 4; + private static final int NAMED_PARAMETER_GROUP = 6; + private static final int COMPARISION_TYPE_GROUP = 1; + + static { + + List keywords = new ArrayList<>(); + + for (ParameterBindingType type : ParameterBindingType.values()) { + if (type.getKeyword() != null) { + keywords.add(type.getKeyword()); + } + } + + StringBuilder builder = new StringBuilder(); + builder.append("("); + builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords + builder.append(")?"); + builder.append("(?: )?"); // some whitespace + builder.append("\\(?"); // optional braces around parameters + builder.append("("); + builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index + builder.append("|"); // or + + // named parameter and the parameter name + builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?"); + + builder.append(")"); + builder.append("\\)?"); // optional braces around parameters + + PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); + } + + /** + * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns + * the cleaned up 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.contains("?#{")) { + parametersShouldBeAccessedByIndex = true; + } + + ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query, + parametersShouldBeAccessedByIndex, parameterLabels); + + String resultingQuery = parsedQuery.getQueryString(); + Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery); + + ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings)); + int currentIndex = 0; + + boolean usesJpaStyleParameters = false; + + while (matcher.find()) { + + if (parsedQuery.isQuoted(matcher.start())) { + continue; + } + + String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP); + String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP); + Integer parameterIndex = getParameterIndex(parameterIndexString); + + String match = matcher.group(0); + if (JDBC_STYLE_PARAM.matcher(match).find()) { + jdbcStyle = true; + } + + if (NUMBERED_STYLE_PARAM.matcher(match) + .find() || NAMED_STYLE_PARAM.matcher(match).find()) { + usesJpaStyleParameters = true; + } + + if (usesJpaStyleParameters && jdbcStyle) { + throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported"); + } + + String typeSource = matcher.group(COMPARISION_TYPE_GROUP); + Assert.isTrue(parameterIndexString != null || parameterName != null, + () -> String.format("We need either a name or an index; Offending query string: %s", query)); + ValueExpression expression = parsedQuery + .getParameter(parameterName == null ? parameterIndexString : parameterName); + String replacement = null; + + // this only happens for JDBC-style parameters. + if ("".equals(parameterIndexString)) { + parameterIndex = parameterLabels.allocate(); + } + + ParameterBinding.BindingIdentifier queryParameter; + if (parameterIndex != null) { + queryParameter = ParameterBinding.BindingIdentifier.of(parameterIndex); + } + else if (parameterName != null) { + queryParameter = ParameterBinding.BindingIdentifier.of(parameterName); + } + else { + throw new IllegalStateException("No bindable expression found"); + } + ParameterBinding.ParameterOrigin origin = ObjectUtils.isEmpty(expression) + ? ParameterBinding.ParameterOrigin.ofParameter(parameterName, parameterIndex) + : ParameterBinding.ParameterOrigin.ofExpression(expression); + + ParameterBinding.BindingIdentifier targetBinding = queryParameter; + Function bindingFactory = switch (ParameterBindingType + .of(typeSource)) { + case LIKE -> { + + Part.Type likeType = ParameterBinding.LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); + yield (identifier) -> new ParameterBinding.LikeParameterBinding(identifier, origin, likeType); + } + 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 { + targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory, parameterLabels); + } + + replacement = targetBinding.hasName() ? ":" + targetBinding.getName() + : ((!usesJpaStyleParameters && jdbcStyle) ? "?" : "?" + targetBinding.getPosition()); + String result; + String substring = matcher.group(2); + + int index = resultingQuery.indexOf(substring, currentIndex); + if (index < 0) { + result = resultingQuery; + } + else { + currentIndex = index + replacement.length(); + result = resultingQuery.substring(0, index) + replacement + + resultingQuery.substring(index + substring.length()); + } + + resultingQuery = result; + } + + parameterBindingPostProcessor.accept(bindings); + return new PreprocessedQuery(declaredQueryFactory.apply(resultingQuery), bindings, jdbcStyle, + containsPageableInSpel); + } + + private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, + boolean parametersShouldBeAccessedByIndex, IndexedParameterLabels parameterLabels) { + + /* + * If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to + * not mix-up with the actual parameter indices. + */ + BiFunction indexToParameterName = parametersShouldBeAccessedByIndex + ? (index, expression) -> String.valueOf(parameterLabels.allocate()) + : (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1); + + String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":"; + + BiFunction parameterNameToReplacement = (prefix, name) -> fixedPrefix + name; + ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(ValueExpressionParser.create(), + indexToParameterName, parameterNameToReplacement); + + return rewriter.parse(queryWithSpel); + } + + private static @Nullable Integer getParameterIndex(@Nullable String parameterIndexString) { + + if (parameterIndexString == null || parameterIndexString.isEmpty()) { + return null; + } + return Integer.valueOf(parameterIndexString); + } + + private static Set findParameterIndices(String query) { + + Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query); + Set usedParameterIndices = new TreeSet<>(); + + while (parameterIndexMatcher.find()) { + + String parameterIndexString = parameterIndexMatcher.group(1); + Integer parameterIndex = getParameterIndex(parameterIndexString); + if (parameterIndex != null) { + usedParameterIndices.add(parameterIndex); + } + } + + return usedParameterIndices; + } + + private static void checkAndRegister(ParameterBinding binding, List bindings) { + + bindings.stream() // + .filter(it -> it.bindsTo(binding)) // + .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding))); + + if (!bindings.contains(binding)) { + bindings.add(binding); + } + } + + /** + * An enum for the different types of bindings. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private enum ParameterBindingType { + + // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace + // character, while = does not. + LIKE("like "), IN("in "), AS_IS(null); + + private final @Nullable String keyword; + + ParameterBindingType(@Nullable String keyword) { + this.keyword = keyword; + } + + /** + * Returns the keyword that will trigger the binding type or {@literal null} if the type is not triggered by a + * keyword. + * + * @return the keyword + */ + public @Nullable String getKeyword() { + return keyword; + } + + /** + * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal #AS_IS} in + * case no other {@link ParameterBindingType} could be found. + */ + static ParameterBindingType of(String typeSource) { + + if (!StringUtils.hasText(typeSource)) { + return AS_IS; + } + + for (ParameterBindingType type : values()) { + if (type.name().equalsIgnoreCase(typeSource.trim())) { + return type; + } + } + + throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s", typeSource)); + } + } + } + + /** + * 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 ParameterBinding.LikeParameterBinding#prepare(Object) LIKE + * rewrite}. + * + * @author Mark Paluch + * @since 3.1.2 + */ + private static class ParameterBindings { + + private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); + + private final Consumer registration; + + public ParameterBindings(List bindings, Consumer registration) { + + for (ParameterBinding binding : bindings) { + this.methodArgumentToLikeBindings.put(binding.getIdentifier(), new ArrayList<>(List.of(binding))); + } + + this.registration = registration; + } + + /** + * @param identifier + * @return whether the identifier is already bound. + */ + public boolean isBound(ParameterBinding.BindingIdentifier identifier) { + return !getBindings(identifier).isEmpty(); + } + + ParameterBinding.BindingIdentifier register(ParameterBinding.BindingIdentifier identifier, + ParameterBinding.ParameterOrigin origin, + Function bindingFactory, + IndexedParameterLabels parameterLabels) { + + Assert.isInstanceOf(ParameterBinding.MethodInvocationArgument.class, origin); + + ParameterBinding.BindingIdentifier methodArgument = ((ParameterBinding.MethodInvocationArgument) origin) + .identifier(); + List bindingsForOrigin = getBindings(methodArgument); + + if (!isBound(identifier)) { + + ParameterBinding binding = bindingFactory.apply(identifier); + registration.accept(binding); + bindingsForOrigin.add(binding); + return binding.getIdentifier(); + } + + ParameterBinding binding = bindingFactory.apply(identifier); + + for (ParameterBinding existing : bindingsForOrigin) { + + if (existing.isCompatibleWith(binding)) { + return existing.getIdentifier(); + } + } + + ParameterBinding.BindingIdentifier syntheticIdentifier; + if (identifier.hasName() && methodArgument.hasName()) { + + int index = 0; + String newName = methodArgument.getName(); + while (existsBoundParameter(newName)) { + index++; + newName = methodArgument.getName() + "_" + index; + } + syntheticIdentifier = ParameterBinding.BindingIdentifier.of(newName); + } + else { + syntheticIdentifier = ParameterBinding.BindingIdentifier.of(parameterLabels.allocate()); + } + + ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier); + registration.accept(newBinding); + bindingsForOrigin.add(newBinding); + return newBinding.getIdentifier(); + } + + private boolean existsBoundParameter(String key) { + return methodArgumentToLikeBindings.values().stream() + .flatMap(Collection::stream) + .anyMatch(it -> key.equals(it.getName())); + } + + private List getBindings(ParameterBinding.BindingIdentifier identifier) { + return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>()); + } + + public void register(ParameterBinding parameterBinding) { + 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 145e94150a..2810f957c0 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 @@ -25,7 +25,8 @@ * * @author Diego Krupitza * @author Greg Turnquist - * @since 2.7.0 + * @author Mark Paluch + * @since 2.7 */ public interface QueryEnhancer { @@ -80,7 +81,7 @@ static QueryEnhancer create(DeclaredQuery query) { String rewrite(QueryRewriteInformation rewriteInformation); /** - * 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}. 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 984193b926..0233798594 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,7 +16,7 @@ package org.springframework.data.jpa.repository.query; /** - * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link StructuredQuery}. + * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link ParametrizedQuery}. * * @author Diego Krupitza * @author Greg Turnquist 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 31f1fadc83..6d6196b8ef 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 @@ -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, StructuredQuery parametrizedQuery); + abstract @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery parametrizedQuery); /** * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. @@ -180,7 +180,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, StructuredQuery parametrizedQuery) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery parametrizedQuery) { if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) { return null; @@ -212,7 +212,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory { @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, StructuredQuery query) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) { if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) { return null; @@ -248,7 +248,7 @@ private static class BasicQueryParameterSetterFactory extends QueryParameterSett } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, StructuredQuery query) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) { Assert.notNull(binding, "Binding must not be null"); @@ -294,7 +294,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, StructuredQuery 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/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index 6c2919e91a..b16d2ef5dd 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 @@ private static String toJpaDirection(Order order) { * * @param query must not be {@literal null}. * @return Might return {@literal null}. - * @deprecated use {@link StructuredQuery#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 static Query applyAndBind(String queryString, Iterable entities, E * * @param originalQuery must not be {@literal null} or empty. * @return Guaranteed to be not {@literal null}. - * @deprecated use {@link StructuredQuery#deriveCountQuery(String)} instead. */ - @Deprecated - public static String createCountQueryFor(String originalQuery) { + static String createCountQueryFor(String originalQuery) { return createCountQueryFor(originalQuery, null); } @@ -568,10 +564,8 @@ public static String createCountQueryFor(String originalQuery) { * @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 StructuredQuery#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/StructuredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java deleted file mode 100644 index 9bb4aea060..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2018-2024 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; - -/** - * 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 4.0 - * @see EntityQuery - * @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector) - * @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration) - */ -interface StructuredQuery extends QueryProvider { - - /** - * @return whether the underlying query has at least one parameter. - */ - boolean hasParameterBindings(); - - /** - * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or - * name. - * - * @return Whether the query uses JDBC style parameters. - * @since 2.0.6 - */ - 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/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java index ea5082d9f1..874ff77c99 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java @@ -164,7 +164,7 @@ void rewritesNamedLikeToUniqueParametersIfNecessary() { @Test // GH-3784 void rewritesNamedLikeToUniqueParametersRetainingCountQuery() { - StructuredQuery query = new TestEntityQuery( + 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); @@ -197,7 +197,7 @@ void rewritesNamedLikeToUniqueParametersRetainingCountQuery() { @Test // GH-3784 void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() { - StructuredQuery query = new TestEntityQuery( + ParametrizedQuery query = new TestEntityQuery( "select u from User u where u.firstname like %:#{firstname} or u.firstname like :#{firstname}%", false) .deriveCountQuery(null); @@ -355,7 +355,7 @@ void detectsMultipleNamedInParameterBindings() { void deriveCountQueryWithNamedInRetainsOrigin() { String queryString = "select u from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins)"; - StructuredQuery query = new TestEntityQuery(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 +376,7 @@ void deriveCountQueryWithNamedInRetainsOrigin() { void deriveCountQueryWithPositionalInRetainsOrigin() { String queryString = "select u from User u where (?1) IS NULL OR LOWER(u.login) IN (?1)"; - StructuredQuery query = new TestEntityQuery(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)"); @@ -464,7 +464,7 @@ void countQueryDerivationRetainsNamedExpressionParameters() { "select u from User u where foo = :#{bar} ORDER BY CASE WHEN (u.firstname >= :#{name}) THEN 0 ELSE 1 END", false); - StructuredQuery countQuery = query.deriveCountQuery(null); + ParametrizedQuery countQuery = query.deriveCountQuery(null); assertThat(countQuery.getParameterBindings()).hasSize(1); assertThat(countQuery.getParameterBindings()).extracting(ParameterBinding::getOrigin) @@ -489,7 +489,7 @@ void countQueryDerivationRetainsIndexedExpressionParameters() { "select u from User u where foo = ?#{bar} ORDER BY CASE WHEN (u.firstname >= ?#{name}) THEN 0 ELSE 1 END", false); - StructuredQuery countQuery = query.deriveCountQuery(null); + ParametrizedQuery countQuery = query.deriveCountQuery(null); assertThat(countQuery.getParameterBindings()).hasSize(1); assertThat(countQuery.getParameterBindings()).extracting(ParameterBinding::getOrigin) @@ -932,7 +932,7 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) { DeclaredQuery source = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); - ParametrizedQuery bindableQuery = ParametrizedQuery.ParameterBindingParser.INSTANCE.parse(source.getQueryString(), + PreprocessedQuery bindableQuery = PreprocessedQuery.ParameterBindingParser.INSTANCE.parse(source.getQueryString(), source::rewrite, it -> {}); assertThat(bindableQuery.getBindings().stream().anyMatch(it -> it.getIdentifier().hasName())) // diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java index f260302121..25c0848908 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java @@ -29,7 +29,7 @@ class TestEntityQuery extends DefaultEntityQuery { */ TestEntityQuery(String query, boolean isNative) { - super(ParametrizedQuery.parse(isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query)), + 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 448d4dbbe4..bc05eca3ac 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -170,86 +170,6 @@ public interface UserRepository extends JpaRepository { ---- ==== -[[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.advanced-like]] === Using Advanced `LIKE` Expressions @@ -305,17 +225,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. @@ -341,8 +250,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 @@ -437,14 +458,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: