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 e4b43d72dc..2ba90b96c3 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 @@ -27,6 +27,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; @@ -173,4 +174,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 7188c0c184..0f1e2efe94 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 @@ -119,6 +119,11 @@ public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSo builder.addPropertyReference("entityManager", entityManagerRefs.get(source)); 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 f219c374d2..8e3f5dfb2f 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 @@ -48,8 +48,8 @@ */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { - private final DeclaredQuery query; - private final Lazy countQuery; + private final IntrospectedQuery query; + private final Lazy countQuery; private final QueryMethodEvaluationContextProvider evaluationContextProvider; private final SpelExpressionParser parser; private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache(); @@ -65,31 +65,20 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * @param em must not be {@literal null}. * @param queryString must not be {@literal null}. * @param countQueryString must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @param parser must not be {@literal null}. - * @param queryRewriter must not be {@literal null}. + * @param queryConfiguration must not be {@literal null}. */ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, QueryRewriter queryRewriter, - QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { + @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) { super(method, em); - Assert.hasText(queryString, "Query string must not be null or empty"); - Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null"); - Assert.notNull(parser, "Parser must not be null"); - Assert.notNull(queryRewriter, "QueryRewriter must not be null"); - - this.evaluationContextProvider = evaluationContextProvider; - this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser, - method.isNativeQuery()); + this.evaluationContextProvider = queryConfiguration.getEvaluationContextProvider(); + this.query = ExpressionBasedStringQuery.create(queryString, method, queryConfiguration); this.countQuery = Lazy.of(() -> { if (StringUtils.hasText(countQueryString)) { - - return new ExpressionBasedStringQuery(countQueryString, method.getEntityInformation(), parser, - method.isNativeQuery()); + return ExpressionBasedStringQuery.create(countQueryString, method, queryConfiguration); } return query.deriveCountQuery(method.getCountQueryProjection()); @@ -99,8 +88,8 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri return this.createBinder(this.countQuery.get()); }); - this.parser = parser; - this.queryRewriter = queryRewriter; + this.parser = queryConfiguration.getParser(); + this.queryRewriter = queryConfiguration.getQueryRewriter(method); JpaParameters parameters = method.getParameters(); if (parameters.hasPageableParameter() || parameters.hasSortParameter()) { @@ -135,7 +124,7 @@ protected ParameterBinder createBinder() { return createBinder(query); } - protected ParameterBinder createBinder(DeclaredQuery query) { + protected ParameterBinder createBinder(IntrospectedQuery query) { return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, parser, evaluationContextProvider); } @@ -160,14 +149,14 @@ protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) { /** * @return the query */ - public DeclaredQuery getQuery() { + public IntrospectedQuery getQuery() { return query; } /** * @return the countQuery */ - public DeclaredQuery getCountQuery() { + public IntrospectedQuery getCountQuery() { return countQuery.get(); } @@ -217,7 +206,7 @@ String applySorting(CachableQuery cachableQuery) { * Query Sort Rewriter interface. */ interface QuerySortRewriter { - String getSorted(DeclaredQuery query, Sort sort); + String getSorted(IntrospectedQuery query, Sort sort); } /** @@ -226,7 +215,7 @@ interface QuerySortRewriter { enum NoOpQuerySortRewriter implements QuerySortRewriter { INSTANCE; - public String getSorted(DeclaredQuery query, Sort sort) { + public String getSorted(IntrospectedQuery query, Sort sort) { if (sort.isSorted()) { throw new UnsupportedOperationException("NoOpQueryCache does not support sorting"); @@ -245,7 +234,7 @@ class CachingQuerySortRewriter implements QuerySortRewriter { AbstractStringBasedJpaQuery.this::applySorting); @Override - public String getSorted(DeclaredQuery query, Sort sort) { + public String getSorted(IntrospectedQuery query, Sort sort) { if (sort.isUnsorted()) { return query.getQueryString(); @@ -264,19 +253,19 @@ public String getSorted(DeclaredQuery query, Sort sort) { */ static class CachableQuery { - private final DeclaredQuery declaredQuery; + private final IntrospectedQuery introspectedQuery; private final String queryString; private final Sort sort; - CachableQuery(DeclaredQuery query, Sort sort) { + CachableQuery(IntrospectedQuery query, Sort sort) { - this.declaredQuery = query; + this.introspectedQuery = query; this.queryString = query.getQueryString(); this.sort = sort; } - DeclaredQuery getDeclaredQuery() { - return declaredQuery; + IntrospectedQuery getDeclaredQuery() { + return introspectedQuery; } Sort getSort() { @@ -285,7 +274,7 @@ Sort getSort() { @Nullable String getAlias() { - return declaredQuery.getAlias(); + return introspectedQuery.getAlias(); } @Override 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 4e54424404..6d1a691a33 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 @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * 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. @@ -15,99 +15,42 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.List; - -import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; - /** - * 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 - * @since 2.0.3 + * @author Mark Paluch */ -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 jpql(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. + * Creates a DeclaredQuery for a native query. * - * @since 1.10 + * @param query the native query string. + * @return */ - 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. - * - * @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 - */ - default boolean usesPaging() { - return false; + static DeclaredQuery nativeQuery(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..4ce3f4a136 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java @@ -0,0 +1,66 @@ +/* + * 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; + } + + public String getQueryString() { + return query; + } + + 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 80% 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 67f9f9b3e6..8b6041174f 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 @@ -21,17 +21,17 @@ import org.springframework.lang.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 IntrospectedQuery { /** * An implementation implementing the NULL-Object pattern for situations where there is no query. */ - static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery(); + static final IntrospectedQuery EMPTY_QUERY = new EmptyIntrospectedQuery(); @Override public boolean hasNamedParameter() { @@ -43,6 +43,11 @@ public String getQueryString() { return ""; } + @Override + public boolean isNativeQuery() { + return false; + } + @Override public String getAlias() { return null; @@ -64,7 +69,7 @@ public List getParameterBindings() { } @Override - public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { + public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) { return EMPTY_QUERY; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java index 411c2662d5..20ccc5629c 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 @@ -27,7 +27,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. *
      @@ -57,25 +57,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, SpelExpressionParser 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, - SpelExpressionParser parser, boolean nativeQuery) { - return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery); + ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, SpelExpressionParser parser, + boolean nativeQuery, QueryEnhancerSelector selector) { + super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query), + selector); } /** @@ -121,4 +109,10 @@ private static String potentiallyQuoteExpressionsParameter(String query) { private static boolean containsExpression(String query) { return query.contains(ENTITY_NAME_VARIABLE_EXPRESSION); } + + public static IntrospectedQuery create(String query, JpaQueryMethod method, JpaQueryConfiguration queryContext) { + return new ExpressionBasedStringQuery(query, method.getEntityInformation(), queryContext.getParser(), + 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..9f535c1ba3 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java @@ -0,0 +1,113 @@ +/* + * 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; + +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 IntrospectedQuery extends DeclaredQuery { + + /** + * Creates a {@literal DeclaredQuery} from a query {@literal String}. + * + * @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. + */ + static IntrospectedQuery of(@Nullable String query, boolean nativeQuery) { + return of(query, nativeQuery, QueryEnhancerSelector.DEFAULT_SELECTOR); + } + + /** + * Creates a {@literal DeclaredQuery} from a query {@literal String}. + * + * @param query might be {@literal null} or empty. + * @param nativeQuery is a given query is native or not + * @param selector + * @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument. + */ + static IntrospectedQuery of(@Nullable String query, boolean nativeQuery, QueryEnhancerSelector selector) { + return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY + : new StringQuery(query, nativeQuery, selector); + } + + /** + * @return whether the underlying query has at least one named parameter. + */ + boolean hasNamedParameter(); + + /** + * 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. + * + * @param countQueryProjection an optional return type for the query. + * @return a new {@literal DeclaredQuery} instance. + */ + IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection); + + /** + * @return whether paging is implemented in the query itself, e.g. using SpEL expressions. + * @since 2.0.6 + */ + default boolean usesPaging() { + return false; + } + + /** + * 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..5100131fdf --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java @@ -0,0 +1,65 @@ +/* + * 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.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * 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 QueryMethodEvaluationContextProvider evaluationContextProvider; + private final SpelExpressionParser parser; + + public JpaQueryConfiguration(QueryRewriterProvider queryRewriter, QueryEnhancerSelector selector, + QueryMethodEvaluationContextProvider evaluationContextProvider, EscapeCharacter escapeCharacter, + SpelExpressionParser parser) { + + this.queryRewriter = queryRewriter; + this.selector = selector; + this.escapeCharacter = escapeCharacter; + this.evaluationContextProvider = evaluationContextProvider; + this.parser = parser; + } + + public QueryRewriter getQueryRewriter(JpaQueryMethod queryMethod) { + return queryRewriter.getQueryRewriter(queryMethod); + } + + public QueryEnhancerSelector getSelector() { + return selector; + } + + public EscapeCharacter getEscapeCharacter() { + return escapeCharacter; + } + + public QueryMethodEvaluationContextProvider getEvaluationContextProvider() { + return evaluationContextProvider; + } + + public SpelExpressionParser getParser() { + return parser; + } +} 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 c677b2efcc..860badb518 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 @@ -29,9 +29,9 @@ import org.antlr.v4.runtime.TokenStream; import org.antlr.v4.runtime.atn.PredictionMode; import org.antlr.v4.runtime.tree.ParseTreeVisitor; + import org.springframework.data.domain.Sort; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * Implementation of {@link QueryEnhancer} to enhance JPA queries using ANTLR parsers. @@ -97,43 +97,34 @@ static void configureParser(String query, Lexer lexer, Parser parser) { } /** - * 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 8f1d3247df..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2013-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 jakarta.persistence.EntityManager; - -import org.springframework.data.jpa.repository.QueryRewriter; -import org.springframework.data.repository.query.QueryCreationException; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; - -/** - * Factory to create the appropriate {@link RepositoryQuery} for a {@link JpaQueryMethod}. - * - * @author Thomas Darimont - * @author Mark Paluch - */ -enum JpaQueryFactory { - - INSTANCE; - - private static final SpelExpressionParser PARSER = new SpelExpressionParser(); - - /** - * Creates a {@link RepositoryQuery} from the given {@link String} query. - * - * @param method must not be {@literal null}. - * @param em must not be {@literal null}. - * @param countQueryString - * @param queryString must not be {@literal null}. - * @param evaluationContextProvider - * @return - */ - AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, QueryRewriter queryRewriter, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - - if (method.isScrollQuery()) { - throw QueryCreationException.create(method, "Scroll queries are not supported using String-based queries"); - } - - return method.isNativeQuery() - ? new NativeJpaQuery(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider, - PARSER) - : new SimpleJpaQuery(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider, - PARSER); - } - - /** - * 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 8cf8d12125..ec410acae3 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 @@ -22,15 +22,16 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -69,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); } @@ -108,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) { - - super(em, queryMethodFactory, queryRewriterProvider); + JpaQueryConfiguration configuration) { - 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()); } } @@ -133,31 +128,27 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer * @author Thomas Darimont * @author Jens Schauder */ - private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy { - - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + 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 evaluationContextProvider must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - QueryMethodEvaluationContextProvider evaluationContextProvider, QueryRewriterProvider queryRewriterProvider) { - - super(em, queryMethodFactory, queryRewriterProvider); + JpaQueryConfiguration configuration) { - this.evaluationContextProvider = evaluationContextProvider; + 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())) { @@ -167,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, evaluationContextProvider); + 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, evaluationContextProvider); + 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 // @@ -209,6 +200,44 @@ private String getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, E 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); + } + } /** @@ -231,31 +260,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); } } @@ -272,20 +299,33 @@ public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory @Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider, QueryRewriterProvider queryRewriterProvider, EscapeCharacter escape) { + return create(em, queryMethodFactory, key, new JpaQueryConfiguration(queryRewriterProvider, + QueryEnhancerSelector.DEFAULT_SELECTOR, evaluationContextProvider, escape, new SpelExpressionParser())); + } + + /** + * Creates a {@link QueryLookupStrategy} for the given {@link EntityManager} and {@link Key}. + * + * @param em must not be {@literal null}. + * @param queryMethodFactory must not be {@literal null}. + * @param key may be {@literal null}. + * @param configuration must not be {@literal null}. + */ + public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory, + @Nullable Key key, JpaQueryConfiguration configuration) { + Assert.notNull(em, "EntityManager must not be null"); - Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null"); + Assert.notNull(configuration, "JpaQueryConfiguration must not be null"); switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) { case CREATE: - return new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape); + return new CreateQueryLookupStrategy(em, queryMethodFactory, configuration); case USE_DECLARED_QUERY: - return new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider, - queryRewriterProvider); + return new DeclaredQueryLookupStrategy(em, queryMethodFactory, configuration); case CREATE_IF_NOT_FOUND: return new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory, - new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape), - new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider, 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/JpaQueryMethod.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index fde3b2cb9a..885b989bbe 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 @@ -158,8 +158,6 @@ private static Class potentiallyUnwrapReturnTypeFor(RepositoryMetadata metada return returnType.getType(); } - - @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public JpaEntityMetadata getEntityInformation() { 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 99bf03c437..7ebe816ecb 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 @@ -22,6 +22,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryCreationException; @@ -51,13 +53,13 @@ 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 introspectedQuery; private final QueryParameterSetter.QueryMetadataCache metadataCache; /** * Creates a new {@link NamedQuery}. */ - private NamedQuery(JpaQueryMethod method, EntityManager em) { + private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelector selector) { super(method, em); @@ -90,9 +92,12 @@ private NamedQuery(JpaQueryMethod method, EntityManager em) { } String queryString = extractor.extractQueryString(query); + org.springframework.data.jpa.repository.Query queryAnnotation = AnnotatedElementUtils + .findMergedAnnotation(method.getMethod(), org.springframework.data.jpa.repository.Query.class); // TODO: Detect whether a named query is a native one. - this.declaredQuery = Lazy.of(() -> DeclaredQuery.of(queryString, query.toString().contains("NativeQuery"))); + this.introspectedQuery = Lazy.of(() -> IntrospectedQuery.of(queryString, + (queryAnnotation != null && queryAnnotation.nativeQuery()) || query.toString().contains("NativeQuery"))); this.metadataCache = new QueryParameterSetter.QueryMetadataCache(); } @@ -126,9 +131,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}. */ @Nullable - public static RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em) { + public static RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em, QueryEnhancerSelector selector) { final String queryName = method.getNamedQueryName(); @@ -146,7 +152,7 @@ public static RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em try { - 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)); } @@ -188,7 +194,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc } else { - String countQueryString = declaredQuery.get().deriveCountQuery(countProjection).getQueryString(); + String countQueryString = introspectedQuery.get().deriveCountQuery(countProjection).getQueryString(); cacheKey = countQueryString; countQuery = em.createQuery(countQueryString, Long.class); } @@ -220,7 +226,7 @@ protected Class getTypeToRead(ReturnedType returnedType) { return type.isInterface() ? Tuple.class : null; } - return declaredQuery.get().hasConstructorExpression() // + return introspectedQuery.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 c0ad69de72..fc6cb95f7f 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 @@ -24,12 +24,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.NativeQuery; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -44,7 +41,7 @@ * @author Mark Paluch * @author Greg Turnquist */ -final class NativeJpaQuery extends AbstractStringBasedJpaQuery { +class NativeJpaQuery extends AbstractStringBasedJpaQuery { private final @Nullable String sqlResultSetMapping; @@ -57,13 +54,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 queryConfiguration must not be {@literal null}. */ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, - QueryRewriter rewriter, QueryMethodEvaluationContextProvider evaluationContextProvider, - SpelExpressionParser parser) { + JpaQueryConfiguration queryConfiguration) { - super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser); + 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 898cd73936..15c10450b0 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 @@ -85,7 +85,7 @@ static ParameterBinder createCriteriaBinder(JpaParameters parameters, 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, declaredQuery); + 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/QueryEnhancerFactories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java new file mode 100644 index 0000000000..8b2a3ac669 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java @@ -0,0 +1,165 @@ +/* + * 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 f7c6f7dd1c..8bb8978a88 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,71 +15,40 @@ */ 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; - /** - * 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 * @since 2.7.0 */ -public final class QueryEnhancerFactory { - - private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class); - - private 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"); - } +public interface QueryEnhancerFactory { - 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."); - } - - } + /** + * Returns whether this QueryEnhancerFactory supports the given {@link DeclaredQuery}. + * + * @param query the query to be enhanced and introspected. + * @return {@code true} if this QueryEnhancer supports the given query; {@code false} otherwise. + */ + boolean supports(DeclaredQuery query); - private QueryEnhancerFactory() {} + /** + * Creates a new {@link QueryEnhancer} for the given query. + * + * @param query the query to be enhanced and introspected. + * @return + */ + QueryEnhancer create(DeclaredQuery query); /** - * Creates a new {@link QueryEnhancer} for the given {@link DeclaredQuery}. + * Creates a new {@link QueryEnhancer} for the given {@link IntrospectedQuery}. * * @param query must not be {@literal null}. * @return an implementation of {@link QueryEnhancer} that suits the query the most */ - public static QueryEnhancer forQuery(DeclaredQuery query) { - - if (query.isNativeQuery()) { - - if (jSqlParserPresent) { - /* - * If JSqlParser fails, throw some alert signaling that people should write a custom Impl. - */ - return new JSqlParserQueryEnhancer(query); - } - - return new DefaultQueryEnhancer(query); - } - - if (PersistenceProvider.HIBERNATE.isPresent()) { - return JpaQueryEnhancer.forHql(query); - } else if (PersistenceProvider.ECLIPSELINK.isPresent()) { - return JpaQueryEnhancer.forEql(query); - } else { - return JpaQueryEnhancer.forJpql(query); - } + static QueryEnhancer 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..2830c31fd4 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java @@ -0,0 +1,97 @@ +/* + * 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 + */ + QueryEnhancer 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 QueryEnhancer select(DeclaredQuery query) { + return selectFactory(query).create(query); + } + + private QueryEnhancerFactory selectFactory(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 9e27184582..646b42657b 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 @@ -48,7 +48,7 @@ abstract class QueryParameterSetterFactory { @Nullable - abstract QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery); + abstract QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery); /** * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. @@ -184,7 +184,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar @Nullable @Override - public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) { + public QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { if (!(binding.getOrigin()instanceof ParameterBinding.Expression e)) { return null; @@ -234,7 +234,7 @@ private static class BasicQueryParameterSetterFactory extends QueryParameterSett } @Override - public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) { + public QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { Assert.notNull(binding, "Binding must not be null"); @@ -245,7 +245,7 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla BindingIdentifier identifier = mia.identifier(); - if (declaredQuery.hasNamedParameter()) { + if (introspectedQuery.hasNamedParameter()) { parameter = findParameterForBinding(parameters, identifier.getName()); } else { parameter = findParameterForBinding(parameters, identifier.getPosition() - 1); @@ -289,7 +289,7 @@ private static class CriteriaQueryParameterSetterFactory extends QueryParameterS } @Override - public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) { + public QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { int parameterIndex = binding.getRequiredPosition() - 1; 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 0265b5549d..a77fa0faf3 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 @@ -446,7 +446,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. */ @Nullable @Deprecated @@ -579,7 +579,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) { @@ -593,7 +593,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 5174168501..2b5cd3b1cc 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,10 +18,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.Query; -import org.springframework.data.jpa.repository.QueryRewriter; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; /** @@ -34,7 +31,7 @@ * @author Mark Paluch * @author Greg Turnquist */ -final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { +class SimpleJpaQuery extends AbstractStringBasedJpaQuery { /** * Creates a new {@link SimpleJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}. @@ -42,13 +39,11 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { * @param method must not be {@literal null} * @param em must not be {@literal null} * @param countQueryString - * @param queryRewriter must not be {@literal null} - * @param evaluationContextProvider must not be {@literal null} - * @param parser must not be {@literal null} + * @param queryConfiguration must not be {@literal null}. */ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString, - QueryRewriter queryRewriter, QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { - this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryRewriter, evaluationContextProvider, parser); + JpaQueryConfiguration queryConfiguration) { + this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryConfiguration); } /** @@ -58,14 +53,12 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String * @param em must not be {@literal null} * @param queryString must not be {@literal null} or empty * @param countQueryString - * @param queryRewriter - * @param evaluationContextProvider must not be {@literal null} - * @param parser must not be {@literal null} + * @param queryConfiguration must not be {@literal null}. */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, QueryRewriter queryRewriter, - QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { + public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, + JpaQueryConfiguration queryConfiguration) { - super(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider, parser); + 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 3b365926db..c8a06358b6 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 @@ -56,7 +56,7 @@ * @author Greg Turnquist * @author Yuriy Tsarkov */ -class StringQuery implements DeclaredQuery { +class StringQuery implements IntrospectedQuery { private final String query; private final List bindings; @@ -64,6 +64,7 @@ class StringQuery implements DeclaredQuery { private final boolean usesJdbcStyleParameters; private final boolean isNative; private final QueryEnhancer queryEnhancer; + private final QueryEnhancerSelector selector; private final boolean hasNamedParameters; /** @@ -72,6 +73,15 @@ class StringQuery implements DeclaredQuery { * @param query must not be {@literal null} or empty. */ public StringQuery(String query, boolean isNative) { + this(query, isNative, QueryEnhancerSelector.DEFAULT_SELECTOR); + } + + /** + * 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) { Assert.hasText(query, "Query must not be null or empty"); @@ -84,7 +94,8 @@ public StringQuery(String query, boolean isNative) { this.bindings, queryMeta); this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; - this.queryEnhancer = QueryEnhancerFactory.forQuery(this); + this.queryEnhancer = selector.select(this); + this.selector = selector; boolean hasNamedParameters = false; for (ParameterBinding parameterBinding : getParameterBindings()) { @@ -114,10 +125,10 @@ public List getParameterBindings() { } @Override - public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { + public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) { StringQuery stringQuery = new StringQuery(this.queryEnhancer.createCountQueryFor(countQueryProjection), // - this.isNative); + this.isNative, this.selector); if (this.hasParameterBindings() && !this.getParameterBindings().equals(stringQuery.getParameterBindings())) { stringQuery.getParameterBindings().clear(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index bffdafd46b..b21d30e244 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 @@ -36,15 +36,7 @@ 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; @@ -60,6 +52,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ReturnedType; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -80,6 +73,8 @@ */ public class JpaRepositoryFactory extends RepositoryFactorySupport { + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + private final EntityManager entityManager; private final QueryExtractor extractor; private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; @@ -87,6 +82,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private QueryEnhancerSelector queryEnhancerSelector = QueryEnhancerSelector.DEFAULT_SELECTOR; private JpaQueryMethodFactory queryMethodFactory; private QueryRewriterProvider queryRewriterProvider; @@ -178,6 +174,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)}. @@ -238,8 +247,10 @@ protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFa protected Optional getQueryLookupStrategy(@Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, evaluationContextProvider, - queryRewriterProvider, escapeCharacter)); + JpaQueryConfiguration configuration = new JpaQueryConfiguration(queryRewriterProvider, queryEnhancerSelector, + evaluationContextProvider, escapeCharacter, PARSER); + + return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, configuration)); } @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 f75f45a8d1..c7d8b2495d 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,10 +18,16 @@ 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.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; @@ -45,10 +51,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 JpaQueryMethodFactory queryMethodFactory; + private @Nullable Function queryEnhancerSelector; /** * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. @@ -74,6 +82,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. @@ -100,6 +114,42 @@ public void setQueryMethodFactory(@Nullable JpaQueryMethodFactory factory) { } } + /** + * 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) { + this.queryEnhancerSelector = bf -> queryEnhancerSelector; + } + + /** + * Configures the {@link QueryEnhancerSelector} to be used. + * + * @param queryEnhancerSelector must not be {@literal null}. + */ + public void setQueryEnhancerSelector(Class queryEnhancerSelector) { + this.queryEnhancerSelector = bf -> { + + if (bf != null) { + + ObjectProvider beanProvider = bf.getBeanProvider(queryEnhancerSelector); + QueryEnhancerSelector selector = beanProvider.getIfAvailable(); + + if (selector != null) { + return selector; + } + + if (bf instanceof AutowireCapableBeanFactory acbf) { + return acbf.createBean(queryEnhancerSelector); + } + } + + return BeanUtils.instantiateClass(queryEnhancerSelector); + }; + } + @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { @@ -113,15 +163,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 (queryEnhancerSelector != null) { + factory.setQueryEnhancerSelector(queryEnhancerSelector.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 6e6c605114..e853861f20 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; @@ -33,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 +53,10 @@ @ContextConfiguration("classpath:infrastructure.xml") class AbstractStringBasedJpaQueryIntegrationTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, QueryMethodEvaluationContextProvider.DEFAULT, EscapeCharacter.DEFAULT, + new SpelExpressionParser()); + @PersistenceContext EntityManager em; @Autowired BeanFactory beanFactory; @@ -66,8 +70,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, - QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser()); + AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, 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 5a33a4bf59..cd3ba37f18 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java @@ -29,12 +29,12 @@ import org.assertj.core.util.Arrays; import org.junit.jupiter.api.Test; import org.mockito.Mockito; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; 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; @@ -55,6 +55,10 @@ */ class AbstractStringBasedJpaQueryUnitTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, QueryMethodEvaluationContextProvider.DEFAULT, EscapeCharacter.DEFAULT, + new SpelExpressionParser()); + @Test // GH-3310 void shouldNotAttemptToAppendSortIfNoSortArgumentPresent() { @@ -117,8 +121,7 @@ static InvocationCapturingStringQueryStub forMethod(Class repository, String Query query = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class); return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery(), - new SpelExpressionParser()); - + CONFIG); } static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQuery { @@ -127,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, SpelExpressionParser parser) { + @Nullable String countQueryString, JpaQueryConfiguration queryContext) { super(queryMethod, new Supplier() { @Override @@ -141,8 +144,7 @@ public EntityManager get() { return em; } - }.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class), - Mockito.mock(QueryMethodEvaluationContextProvider.class), parser); + }.get(), queryString, countQueryString, queryContext); 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 f1d7e707d8..b76dd57206 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 @@ -16,7 +16,7 @@ package org.springframework.data.jpa.repository.query; import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; /** * TCK Tests for {@link DefaultQueryEnhancer}. @@ -26,12 +26,12 @@ public class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override - QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) { - return new DefaultQueryEnhancer(declaredQuery); + QueryEnhancer createQueryEnhancer(DeclaredQuery query) { + return new DefaultQueryEnhancer(query); } @Override - @Test // GH-2511, GH-2773 + @ParameterizedTest // GH-2511, GH-2773 @Disabled("Not properly supported by QueryUtils") void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {} } 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 241a5310b5..3c47e90093 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 52bdcd82d1..4a61597ca6 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 @@ -798,6 +798,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 e92c16eb88..56412b72a9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java @@ -27,7 +27,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -46,7 +48,10 @@ @MockitoSettings(strictness = Strictness.LENIENT) class ExpressionBasedStringQueryUnitTests { - private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, QueryMethodEvaluationContextProvider.DEFAULT, EscapeCharacter.DEFAULT, + new SpelExpressionParser()); + @Mock JpaEntityMetadata metadata; @BeforeEach @@ -58,14 +63,16 @@ void setUp() { void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() { String source = "select u from #{#entityName} u where u.firstname like :firstname"; - StringQuery query = new ExpressionBasedStringQuery(source, metadata, SPEL_PARSER, false); + StringQuery query = new ExpressionBasedStringQuery(source, metadata, CONFIG.getParser(), 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, SPEL_PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata, CONFIG.getParser(), + true, CONFIG.getSelector()); assertThat(query.getAlias()).isEqualTo("u"); assertThat(query.getQueryString()).isEqualTo("select u from User u"); } @@ -78,7 +85,7 @@ void shouldDetectBindParameterCountCorrectly() { + "AND (LOWER(n.server) LIKE LOWER(:#{#networkRequest.server})) OR :#{#networkRequest.server} IS NULL " + "AND (n.createdAt >= :#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=:#{#networkRequest.createdTime.endDateTime}) " + "AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})", - metadata, SPEL_PARSER, false); + metadata, CONFIG.getParser(), false, CONFIG.getSelector()); assertThat(query.getParameterBindings()).hasSize(8); } @@ -91,7 +98,7 @@ void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() { + "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)" + "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})" + "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})", - metadata, SPEL_PARSER, false); + metadata, CONFIG.getParser(), false, CONFIG.getSelector()); assertThat(query.getParameterBindings()).hasSize(8); } @@ -104,7 +111,7 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { + "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)" + "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})" + "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})", - metadata, SPEL_PARSER, true); + metadata, CONFIG.getParser(), true, CONFIG.getSelector()); assertThat(query.isNativeQuery()).isFalse(); } @@ -112,7 +119,8 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { @Test void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { - StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, SPEL_PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, CONFIG.getParser(), + true, CONFIG.getSelector()); assertThat(query.isNativeQuery()).isFalse(); } @@ -120,7 +128,8 @@ void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { @Test void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { - StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, SPEL_PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, CONFIG.getParser(), true, + CONFIG.getSelector()); assertThat(query.isNativeQuery()).isTrue(); } @@ -129,8 +138,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, SPEL_PARSER, - false); + "select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, + CONFIG.getParser(), false, CONFIG.getSelector()); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo( @@ -154,8 +163,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, SPEL_PARSER, - false); + "select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, + CONFIG.getParser(), false, CONFIG.getSelector()); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()) @@ -179,7 +188,7 @@ void indexedExpressionsShouldCreateLikeBindings() { public void doesTemplatingWhenEntityNameSpelIsPresent() { StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from #{#entityName} u", - metadata, SPEL_PARSER, false); + metadata, CONFIG.getParser(), false, CONFIG.getSelector()); assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u"); } @@ -188,7 +197,7 @@ public void doesTemplatingWhenEntityNameSpelIsPresent() { public void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from User u", metadata, - SPEL_PARSER, false); + CONFIG.getParser(), false, CONFIG.getSelector()); assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u"); } @@ -197,7 +206,7 @@ public void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { public void doesTemplatingWhenEntityNameSpelIsPresentForBindParameter() { StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u where name = :#{#something}", - metadata, SPEL_PARSER, false); + metadata, CONFIG.getParser(), 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 7d57ed37ad..e91af74253 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 b4ce21db0a..9793e4c1f7 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 @@ -1091,6 +1091,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 d5dc2b859e..954e559479 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,8 +37,8 @@ public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override - QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) { - return new JSqlParserQueryEnhancer(declaredQuery); + QueryEnhancer createQueryEnhancer(DeclaredQuery query) { + return new JSqlParserQueryEnhancer(query); } @Override 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 4c504e8cc9..c3c4b8994f 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 b867aba845..63162031e4 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 eb04b8de07..f1a2af59fa 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 @@ -815,6 +815,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 ebdd2a8395..2e4a40c4cc 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/QueryEnhancerTckTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java index 3e3465e3d3..1c63416dd0 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.nativeQuery(query)); String countQueryFor = enhancer.createCountQueryFor(); // lenient cleanup to allow for rendering variance @@ -116,8 +115,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.jpql(query)); String countQueryFor = enhancer.createCountQueryFor(null); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -176,8 +174,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.nativeQuery(query)); String countQueryFor = enhancer.createCountQueryFor(); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -207,6 +204,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 d476c445b2..0d580db52c 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"); @@ -538,7 +538,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)); } @@ -696,7 +696,7 @@ private static void assertCountQuery(StringQuery originalQuery, String countQuer assertThat(getEnhancer(originalQuery).createCountQueryFor()).isEqualToIgnoringCase(countQuery); } - private static QueryEnhancer getEnhancer(DeclaredQuery query) { + private static QueryEnhancer getEnhancer(IntrospectedQuery query) { return QueryEnhancerFactory.forQuery(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 51fb6d8d37..9b363e3c7b 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, DeclaredQuery.of("from Employee e", false)); + setterFactory.create(binding, IntrospectedQuery.of("from Employee e", false)); } @Test // DATAJPA-1058 @@ -63,7 +63,7 @@ void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() { assertThatExceptionOfType(IllegalStateException.class) // .isThrownBy(() -> setterFactory.create(binding, - DeclaredQuery.of("from Employee e where e.name = :NamedParameter", false))) // + IntrospectedQuery.of("from Employee e where e.name = :NamedParameter", false))) // .withMessageContaining("Java 8") // .withMessageContaining("@Param") // .withMessageContaining("-parameters"); @@ -82,7 +82,7 @@ void exceptionWhenCriteriaQueryContainsInsufficientAmountOfParameters() { assertThatExceptionOfType(IllegalArgumentException.class) // .isThrownBy(() -> setterFactory.create(binding, - DeclaredQuery.of("from Employee e where e.name = :NamedParameter", false))) // + IntrospectedQuery.of("from Employee e where e.name = :NamedParameter", false))) // .withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query"); } @@ -97,7 +97,8 @@ void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() { when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("from Employee e where e.name = ?1", false))) // + .isThrownBy( + () -> setterFactory.create(binding, IntrospectedQuery.of("from Employee e where e.name = ?1", false))) // .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 3e77c0cd9e..9cb2c59950 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 @@ -38,6 +38,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -46,7 +47,7 @@ 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.query.JpaQueryLookupStrategy.DeclaredQueryLookupStrategy; import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -76,9 +77,11 @@ @MockitoSettings(strictness = Strictness.LENIENT) class SimpleJpaQueryUnitTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, QueryMethodEvaluationContextProvider.DEFAULT, EscapeCharacter.DEFAULT, + new SpelExpressionParser()); + private static final String USER_QUERY = "select u from User u"; - private static final SpelExpressionParser PARSER = new SpelExpressionParser(); - private static final QueryMethodEvaluationContextProvider EVALUATION_CONTEXT_PROVIDER = QueryMethodEvaluationContextProvider.DEFAULT; private JpaQueryMethod method; @@ -122,8 +125,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, EVALUATION_CONTEXT_PROVIDER, PARSER); + SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, CONFIG); assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {}))) .isEqualTo(typedQuery); @@ -138,7 +140,7 @@ void doesNotApplyPaginationToCountQuery() throws Exception { JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null, - QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER, PARSER); + CONFIG); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -152,9 +154,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, - EVALUATION_CONTEXT_PROVIDER); + AbstractJpaQuery jpaQuery = DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, + queryMethod.getAnnotatedQuery(), null, CONFIG); assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); @@ -172,9 +173,7 @@ 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, - EVALUATION_CONTEXT_PROVIDER); + AbstractJpaQuery jpaQuery = createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(), null); assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); @@ -293,8 +292,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, - EVALUATION_CONTEXT_PROVIDER, PARSER); + "select count(u.id) from #{#entityName} u", CONFIG); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -306,16 +304,17 @@ 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, EVALUATION_CONTEXT_PROVIDER); + return 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 { @@ -350,8 +349,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 f54272b0a7..a2ad669ce1 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 @@ -707,12 +707,12 @@ void usingGreaterThanWithNamedParameter() { void checkNumberOfNamedParameters(String query, int expectedSize, String label, boolean nativeQuery) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, nativeQuery); + IntrospectedQuery introspectedQuery = IntrospectedQuery.of(query, nativeQuery); - assertThat(declaredQuery.hasNamedParameter()) // + assertThat(introspectedQuery.hasNamedParameter()) // .describedAs("hasNamed Parameter " + label) // .isEqualTo(expectedSize > 0); - assertThat(declaredQuery.getParameterBindings()) // + assertThat(introspectedQuery.getParameterBindings()) // .describedAs("parameterBindings " + label) // .hasSize(expectedSize); }