From 264472ba0f43331bb79ac2c083551739ba940789 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Tue, 22 Mar 2022 09:56:05 -0500 Subject: [PATCH] Introduce QueryRewriter. Allow a QueryRewriter to be applied to any query crafted using @Query via an additional @QueryRewriter annotation. Also supported directly inside @Query. See #2162. --- .../data/jpa/repository/Query.java | 17 +- .../data/jpa/repository/QueryRewriter.java | 64 +++++ .../jpa/repository/cdi/JpaRepositoryBean.java | 21 +- .../cdi/QueryRewriterBeanManagerProvider.java | 55 ++++ .../query/AbstractStringBasedJpaQuery.java | 48 +++- .../jpa/repository/query/JpaQueryFactory.java | 10 +- .../query/JpaQueryLookupStrategy.java | 47 ++-- .../jpa/repository/query/JpaQueryMethod.java | 18 +- .../jpa/repository/query/NativeJpaQuery.java | 13 +- .../QueryRewriterBeanFactoryProvider.java | 50 ++++ .../query/QueryRewriterNoopProvider.java | 45 ++++ .../query/QueryRewriterProvider.java | 71 ++++++ .../jpa/repository/query/SimpleJpaQuery.java | 12 +- .../support/JpaRepositoryFactory.java | 52 +++- .../cdi/CdiExtensionIntegrationTests.java | 8 +- ...aQueryRewriterWithCdiIntegrationTests.java | 234 ++++++++++++++++++ ...ctStringBasedJpaQueryIntegrationTests.java | 24 +- .../JpaQueryLookupStrategyUnitTests.java | 20 +- .../JpaQueryRewriteIntegrationTests.java | 218 ++++++++++++++++ .../query/SimpleJpaQueryUnitTests.java | 28 +-- src/main/asciidoc/jpa.adoc | 2 +- 21 files changed, 964 insertions(+), 93 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/QueryRewriter.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/QueryRewriterBeanManagerProvider.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterBeanFactoryProvider.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterNoopProvider.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterProvider.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/cdi/JpaQueryRewriterWithCdiIntegrationTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java index f12e7c39c..614bee69c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java @@ -29,7 +29,7 @@ import org.springframework.data.annotation.QueryAnnotation; * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl - * + * @author Greg Turnquist * @see Modifying */ @Retention(RetentionPolicy.RUNTIME) @@ -45,7 +45,8 @@ public @interface Query { /** * Defines a special count query that shall be used for pagination queries to lookup the total number of elements for - * a page. If none is configured we will derive the count query from the original query or {@link #countProjection()} query if any. + * a page. If none is configured we will derive the count query from the original query or {@link #countProjection()} + * query if any. */ String countQuery() default ""; @@ -70,11 +71,19 @@ public @interface Query { String name() default ""; /** - * Returns the name of the {@link jakarta.persistence.NamedQuery} to be used to execute count queries when pagination is - * used. Will default to the named query name configured suffixed by {@code .count}. + * Returns the name of the {@link jakarta.persistence.NamedQuery} to be used to execute count queries when pagination + * is used. Will default to the named query name configured suffixed by {@code .count}. * * @see #name() * @return */ String countName() default ""; + + /** + * Define the {@link QueryRewriter} bean that should be applied to this query after the query is full assembled. + * + * @return + * @since 3.0 + */ + Class queryRewriter() default QueryRewriter.NoopQueryRewriter.class; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/QueryRewriter.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/QueryRewriter.java new file mode 100644 index 000000000..d80a998c2 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/QueryRewriter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2008-2022 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; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +/** + * Callback to rewrite a query right before it's handed to the EntityManager. + * + * @author Greg Turnquist + * @since 3.0 + */ +@FunctionalInterface +public interface QueryRewriter { + + /** + * The assembled query and current {@link Sort} settings are offered. This is the query right before it's handed to + * the EntityManager, so everything that Spring Data and tools intends to do has been done. The user is able to make + * any last minute changes.
+ *
+ * WARNING: No checks are performed before the transformed query is passed to the EntityManager. + * + * @param query - the assembled generated query, right before it's handed over to the EntityManager. + * @param sort - current {@link Sort} settings provided by the method, or {@link Sort#unsorted()}} if there are none. + * @return alter the query however you like. + */ + String rewrite(String query, Sort sort); + + /** + * This alternative is used to handle {@link Pageable}-based methods. + * + * @param query - the assembled generated query, right before it's handed over to the EntityManager. + * @param pageRequest + * @return + */ + default String rewrite(String query, Pageable pageRequest) { + return rewrite(query, pageRequest.getSort()); + } + + /** + * A {@link QueryRewriter} that doesn't change the query. + */ + public class NoopQueryRewriter implements QueryRewriter { + + @Override + public String rewrite(String query, Sort sort) { + return query; + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/JpaRepositoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/JpaRepositoryBean.java index 84dec88f9..3f9b522d2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/JpaRepositoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/JpaRepositoryBean.java @@ -15,18 +15,21 @@ */ package org.springframework.data.jpa.repository.cdi; -import java.lang.annotation.Annotation; -import java.util.Optional; -import java.util.Set; - import jakarta.enterprise.context.spi.CreationalContext; import jakarta.enterprise.inject.spi.Bean; import jakarta.enterprise.inject.spi.BeanManager; import jakarta.persistence.EntityManager; +import java.lang.annotation.Annotation; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import org.springframework.data.jpa.repository.query.QueryRewriterProvider; import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.data.repository.cdi.CdiRepositoryBean; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.util.Assert; /** @@ -41,6 +44,7 @@ import org.springframework.util.Assert; class JpaRepositoryBean extends CdiRepositoryBean { private final Bean entityManagerBean; + private final QueryRewriterProvider queryRewriterProvider; /** * Constructs a {@link JpaRepositoryBean}. @@ -58,6 +62,7 @@ class JpaRepositoryBean extends CdiRepositoryBean { Assert.notNull(entityManagerBean, "EntityManager bean must not be null!"); this.entityManagerBean = entityManagerBean; + this.queryRewriterProvider = new QueryRewriterBeanManagerProvider(beanManager); } @Override @@ -65,6 +70,12 @@ class JpaRepositoryBean extends CdiRepositoryBean { EntityManager entityManager = getDependencyInstance(entityManagerBean, EntityManager.class); - return create(() -> new JpaRepositoryFactory(entityManager), repositoryType); + Supplier repositoryFactorySupportSupplier = () -> { + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + jpaRepositoryFactory.setQueryRewriterProvider(queryRewriterProvider); + return jpaRepositoryFactory; + }; + + return create(repositoryFactorySupportSupplier, repositoryType); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/QueryRewriterBeanManagerProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/QueryRewriterBeanManagerProvider.java new file mode 100644 index 000000000..8a4369da8 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/QueryRewriterBeanManagerProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright 2008-2022 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.cdi; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.jpa.repository.QueryRewriter; +import org.springframework.data.jpa.repository.query.QueryRewriterProvider; + +/** + * A {@link BeanManager}-based {@link QueryRewriterProvider}. + * + * @author Greg Turnquist + * @since 3.0 + */ +public class QueryRewriterBeanManagerProvider extends QueryRewriterProvider { + + private static final Log LOGGER = LogFactory.getLog(QueryRewriterBeanManagerProvider.class); + + private final BeanManager beanManager; + + public QueryRewriterBeanManagerProvider(BeanManager beanManager) { + this.beanManager = beanManager; + } + + @Override + protected QueryRewriter extractQueryRewriterBean(Class queryRewriter) { + + try { + Bean bean = (Bean) beanManager.getBeans(queryRewriter).iterator().next(); + CreationalContext context = beanManager.createCreationalContext(bean); + return (QueryRewriter) beanManager.getReference(bean, queryRewriter, context); + } catch (Exception e) { + LOGGER.error(e.toString()); + return null; + } + } +} 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 2294ee18f..6f4eb5bdb 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 @@ -18,6 +18,13 @@ package org.springframework.data.jpa.repository.query; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; @@ -35,14 +42,18 @@ import org.springframework.util.Assert; * @author David Madden * @author Mark Paluch * @author Diego Krupitza + * @author Greg Turnquist */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { + private static final Log LOGGER = LogFactory.getLog(AbstractStringBasedJpaQuery.class); + private final DeclaredQuery query; private final DeclaredQuery countQuery; private final QueryMethodEvaluationContextProvider evaluationContextProvider; private final SpelExpressionParser parser; private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache(); + private final Supplier queryRewriterSupplier; /** * Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and @@ -57,7 +68,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { */ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider, - SpelExpressionParser parser) { + SpelExpressionParser parser, QueryRewriterProvider queryRewriterProvider) { super(method, em); @@ -74,6 +85,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { method.isNativeQuery()); this.parser = parser; + this.queryRewriterSupplier = queryRewriterProvider.of(method); Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(), "JDBC style parameters (?) are not supported for JPA queries."); @@ -86,7 +98,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { .applySorting(accessor.getSort(), query.getAlias()); ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - Query query = createJpaQuery(sortedQueryString, processor.getReturnedType()); + Query query = createJpaQuery(sortedQueryString, accessor.getSort(), accessor.getPageable(), + processor.getReturnedType()); QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query); @@ -137,18 +150,41 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery} * type. */ - protected Query createJpaQuery(String queryString, ReturnedType returnedType) { + protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable, + ReturnedType returnedType) { EntityManager em = getEntityManager(); if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) { - return em.createQuery(queryString); + return em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable)); } Class typeToRead = getTypeToRead(returnedType); return typeToRead == null // - ? em.createQuery(queryString) // - : em.createQuery(queryString, typeToRead); + ? em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable)) // + : em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable), typeToRead); + } + + /** + * Use the {@link QueryRewriter}, potentially rewrite the query, using relevant {@link Sort} and {@link Pageable} + * information. + * + * @param originalQuery + * @param sort + * @param pageable + * @return + */ + protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nullable Pageable pageable) { + + QueryRewriter queryRewriter = this.queryRewriterSupplier.get(); + + if (queryRewriter == null) { + return originalQuery; + } + + return pageable != null && pageable.isPaged() // + ? queryRewriter.rewrite(originalQuery, pageable) // + : queryRewriter.rewrite(originalQuery, sort); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java index 6eada129d..83df010e5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java @@ -45,12 +45,14 @@ enum JpaQueryFactory { * @return */ AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + @Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider, + QueryRewriterProvider queryRewriterProvider) { return method.isNativeQuery() - ? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER) - : new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER); + ? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER, + queryRewriterProvider) + : new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER, + queryRewriterProvider); } /** 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 0db5d16ba..552c587af 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 @@ -15,13 +15,12 @@ */ package org.springframework.data.jpa.repository.query; -import java.lang.reflect.Method; - import jakarta.persistence.EntityManager; +import java.lang.reflect.Method; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.data.jpa.repository.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; @@ -41,6 +40,7 @@ import org.springframework.util.StringUtils; * @author Thomas Darimont * @author Mark Paluch * @author Réda Housni Alaoui + * @author Greg Turnquist */ public final class JpaQueryLookupStrategy { @@ -61,6 +61,7 @@ public final class JpaQueryLookupStrategy { private final EntityManager em; private final JpaQueryMethodFactory queryMethodFactory; + private final QueryRewriterProvider queryRewriterProvider; /** * Creates a new {@link AbstractQueryLookupStrategy}. @@ -68,13 +69,15 @@ public final class JpaQueryLookupStrategy { * @param em must not be {@literal null}. * @param queryMethodFactory must not be {@literal null}. */ - public AbstractQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory) { + public AbstractQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, + QueryRewriterProvider queryRewriterProvider) { Assert.notNull(em, "EntityManager must not be null!"); Assert.notNull(queryMethodFactory, "JpaQueryMethodFactory must not be null!"); this.em = em; this.queryMethodFactory = queryMethodFactory; + this.queryRewriterProvider = queryRewriterProvider; } @Override @@ -84,6 +87,10 @@ public final class JpaQueryLookupStrategy { } protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries); + + protected QueryRewriterProvider getQueryRewriterSupplier() { + return queryRewriterProvider; + } } /** @@ -97,9 +104,9 @@ public final class JpaQueryLookupStrategy { private final EscapeCharacter escape; public CreateQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - EscapeCharacter escape) { + QueryRewriterProvider queryRewriterProvider, EscapeCharacter escape) { - super(em, queryMethodFactory); + super(em, queryMethodFactory, queryRewriterProvider); this.escape = escape; } @@ -130,9 +137,9 @@ public final class JpaQueryLookupStrategy { * @param evaluationContextProvider must not be {@literal null}. */ public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + QueryMethodEvaluationContextProvider evaluationContextProvider, QueryRewriterProvider queryRewriterProvider) { - super(em, queryMethodFactory); + super(em, queryMethodFactory, queryRewriterProvider); this.evaluationContextProvider = evaluationContextProvider; } @@ -152,14 +159,13 @@ public final class JpaQueryLookupStrategy { } return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getRequiredAnnotatedQuery(), - getCountQuery(method, namedQueries, em), - evaluationContextProvider); + getCountQuery(method, namedQueries, em), evaluationContextProvider, getQueryRewriterSupplier()); } String name = method.getNamedQueryName(); if (namedQueries.hasQuery(name)) { - return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), getCountQuery(method, namedQueries, em), - evaluationContextProvider); + return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), + getCountQuery(method, namedQueries, em), evaluationContextProvider, getQueryRewriterSupplier()); } RepositoryQuery query = NamedQuery.lookupFrom(method, em); @@ -221,9 +227,10 @@ public final class JpaQueryLookupStrategy { * @param lookupStrategy must not be {@literal null}. */ public CreateIfNotFoundQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy) { + CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy, + QueryRewriterProvider queryRewriterProvider) { - super(em, queryMethodFactory); + super(em, queryMethodFactory, queryRewriterProvider); Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null!"); Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null!"); @@ -253,20 +260,22 @@ public final class JpaQueryLookupStrategy { * @param escape must not be {@literal null}. */ public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - @Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider, EscapeCharacter escape) { + @Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider, + QueryRewriterProvider queryRewriterProvider, EscapeCharacter escape) { Assert.notNull(em, "EntityManager must not be null!"); Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!"); switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) { case CREATE: - return new CreateQueryLookupStrategy(em, queryMethodFactory, escape); + return new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape); case USE_DECLARED_QUERY: - return new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider); + return new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider, queryRewriterProvider); case CREATE_IF_NOT_FOUND: return new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory, - new CreateQueryLookupStrategy(em, queryMethodFactory, escape), - new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider)); + new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape), + new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider, queryRewriterProvider), + queryRewriterProvider); default: throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); } 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 2c998d4db..88e085527 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 @@ -15,6 +15,9 @@ */ package org.springframework.data.jpa.repository.query; +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; + import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Arrays; @@ -24,9 +27,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import jakarta.persistence.LockModeType; -import jakarta.persistence.QueryHint; - import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.jpa.provider.QueryExtractor; @@ -35,6 +35,7 @@ import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.Parameter; @@ -57,6 +58,7 @@ import org.springframework.util.StringUtils; * @author Mark Paluch * @author Сергей Цыпанов * @author Réda Housni Alaoui + * @author Greg Turnquist */ public class JpaQueryMethod extends QueryMethod { @@ -430,4 +432,14 @@ public class JpaQueryMethod extends QueryMethod { return storedProcedureAttributes; } + /** + * Returns the {@link QueryRewriter} type. + * + * @return type of the {@link QueryRewriter} + * @since 3.0 + */ + @Nullable + Class getQueryRewriter() { + return getMergedOrDefaultAnnotationValue("queryRewriter", Query.class, Class.class); + } } 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 f03866919..c517ff0ae 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -19,6 +19,8 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import jakarta.persistence.Tuple; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; @@ -35,6 +37,7 @@ import org.springframework.lang.Nullable; * @author Oliver Gierke * @author Jens Schauder * @author Mark Paluch + * @author Greg Turnquist */ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { @@ -48,9 +51,10 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { * @param evaluationContextProvider */ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, - QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { + QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser, + QueryRewriterProvider queryRewriterProvider) { - super(method, em, queryString, countQueryString, evaluationContextProvider, parser); + super(method, em, queryString, countQueryString, evaluationContextProvider, parser, queryRewriterProvider); Parameters parameters = method.getParameters(); @@ -60,12 +64,13 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { } @Override - protected Query createJpaQuery(String queryString, ReturnedType returnedType) { + protected Query createJpaQuery(String queryString, Sort sort, Pageable pageable, ReturnedType returnedType) { EntityManager em = getEntityManager(); Class type = getTypeToQueryFor(returnedType); - return type == null ? em.createNativeQuery(queryString) : em.createNativeQuery(queryString, type); + return type == null ? em.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable)) + : em.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable), type); } @Nullable diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterBeanFactoryProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterBeanFactoryProvider.java new file mode 100644 index 000000000..102dc6c94 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterBeanFactoryProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2008-2022 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.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.data.jpa.repository.QueryRewriter; + +/** + * A {@link BeanFactory}-based {@link QueryRewriterProvider}. + * + * @author Greg Turnquist + * @since 3.0 + */ +public class QueryRewriterBeanFactoryProvider extends QueryRewriterProvider { + + private static final Log LOGGER = LogFactory.getLog(QueryRewriterBeanFactoryProvider.class); + + private final BeanFactory beanFactory; + + public QueryRewriterBeanFactoryProvider(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + protected QueryRewriter extractQueryRewriterBean(Class queryRewriter) { + + try { + return beanFactory.getBean(queryRewriter); + } catch (BeansException e) { + LOGGER.error(e.toString()); + return null; + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterNoopProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterNoopProvider.java new file mode 100644 index 000000000..094e377cd --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterNoopProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright 2008-2022 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.repository.QueryRewriter; + +/** + * {@link QueryRewriterProvider} that does nothing. + * + * @author Greg Turnquist + * @since 3.0 + */ +public class QueryRewriterNoopProvider extends QueryRewriterProvider { + + private static final Log LOGGER = LogFactory.getLog(QueryRewriterNoopProvider.class); + + /** + * Returns {@literal null}, signaling there is no rewriting. + * + * @param queryRewriter class definition to find in the context + * @return {@literal null} since this doesn't actually rewrite anything. + */ + @Override + public QueryRewriter extractQueryRewriterBean(Class queryRewriter) { + + LOGGER.warn("You have NOT configured JpaRepositoryFactory with a QueryRewriterProvider!"); + + return null; + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterProvider.java new file mode 100644 index 000000000..ba8071e5c --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRewriterProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright 2008-2022 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.function.Supplier; + +import org.springframework.data.jpa.repository.QueryRewriter; +import org.springframework.lang.Nullable; + +/** + * Provide a {@link QueryRewriter} based upon the {@link JpaQueryMethod} and the surrounding context (Spring, CDI, etc.) + * + * @author Greg Turnquist + * @since 3.0 + */ +public abstract class QueryRewriterProvider { + + /** + * Using a {@link JpaQueryMethod}, extract a potential {@link QueryRewriter}. Wrap all this in a {@link Supplier} to + * defer the lookup until needed. + * + * @param method - JpaQueryMethod + * @return a {@link Supplier}-wrapped callback to fetch the {@link QueryRewriter} + */ + public Supplier of(JpaQueryMethod method) { + return () -> findQueryRewriter(method); + } + + /** + * Using the {@link org.springframework.data.jpa.repository.QueryRewrite} annotation, look for a {@link QueryRewriter} + * and instantiate one. NOTE: If its {@link QueryRewriter.NoopQueryRewriter}, it will just return {@literal null} and + * NOT do any rewrite operations. + * + * @param method - {@link JpaQueryMethod} that has the annotation details + * @return a {@link QueryRewriter for the method or {@code null} + */ + @Nullable + private QueryRewriter findQueryRewriter(JpaQueryMethod method) { + + Class queryRewriter = method.getQueryRewriter(); + + if (queryRewriter == null || queryRewriter == QueryRewriter.NoopQueryRewriter.class) { + return null; + } + + return extractQueryRewriterBean(queryRewriter); + } + + /** + * Extract an instance of {@link QueryRewriter} from the context. Implementations choose what context means, whether + * that is Spring, CDI, or whatever. + * + * @param queryRewriter + * @return a Java bean that implements {@link QueryRewriter}. {@literal null} is valid if no bean is found. + */ + @Nullable + protected abstract QueryRewriter extractQueryRewriterBean(Class queryRewriter); +} 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 cd0b4f228..6e48f10bd 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 @@ -31,6 +31,7 @@ import org.springframework.lang.Nullable; * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch + * @author Greg Turnquist */ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { @@ -44,8 +45,10 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { * @param parser must not be {@literal null} */ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString, - QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { - this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, evaluationContextProvider, parser); + QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser, + QueryRewriterProvider queryRewriterProvider) { + this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, evaluationContextProvider, parser, + queryRewriterProvider); } /** @@ -59,9 +62,10 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { * @param parser must not be {@literal null} */ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, - QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { + QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser, + QueryRewriterProvider queryRewriterProvider) { - super(method, em, queryString, countQueryString, evaluationContextProvider, parser); + super(method, em, queryString, countQueryString, evaluationContextProvider, parser, queryRewriterProvider); 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/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 9b6f2471e..367d0bf92 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 @@ -17,29 +17,24 @@ package org.springframework.data.jpa.repository.support; import static org.springframework.data.querydsl.QuerydslUtils.*; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Tuple; + import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; import java.util.stream.Stream; -import jakarta.persistence.EntityManager; -import jakarta.persistence.Tuple; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.query.AbstractJpaQuery; -import org.springframework.data.jpa.repository.query.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.*; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; @@ -82,6 +77,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; private JpaQueryMethodFactory queryMethodFactory; + private QueryRewriterProvider queryRewriterProvider; /** * Creates a new {@link JpaRepositoryFactory}. @@ -98,6 +94,12 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { this.entityPathResolver = SimpleEntityPathResolver.INSTANCE; this.queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor); + /** + * Default to {@link QueryRewriterNoopProvider}. If there is a {@link BeanFactory} or {@link BeanManager}, this will + * result in later overriding this with the proper version. + */ + this.queryRewriterProvider = new QueryRewriterNoopProvider(); + addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); addRepositoryProxyPostProcessor((factory, repositoryInformation) -> { @@ -118,6 +120,23 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { this.crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); } + /** + * If a {@link BeanFactory} is being set, this is clearly in a Spring context, and so we can capture the + * {@link QueryRewriterProvider} being a {@link QueryRewriterBeanFactoryProvider}. + * + * @param beanFactory + * @throws BeansException + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + + super.setBeanFactory(beanFactory); + + Assert.notNull(beanFactory, "BeanFactory must not be null!"); + + setQueryRewriterProvider(new QueryRewriterBeanFactoryProvider(beanFactory)); + } + /** * Configures the {@link EntityPathResolver} to be used. Defaults to {@link SimpleEntityPathResolver#INSTANCE}. * @@ -151,6 +170,17 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { this.queryMethodFactory = queryMethodFactory; } + /** + * Configures the {@link QueryRewriterProvider} to be used. Defaults to {@link QueryRewriterNoopProvider}. + * + * @param queryRewriterProvider must not be {@literal null} + */ + public void setQueryRewriterProvider(QueryRewriterProvider queryRewriterProvider) { + + Assert.notNull(queryRewriterProvider, "QueryRewriterProvider must not be null!"); + this.queryRewriterProvider = queryRewriterProvider; + } + @Override protected final JpaRepositoryImplementation getTargetRepository(RepositoryInformation information) { @@ -199,7 +229,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { QueryMethodEvaluationContextProvider evaluationContextProvider) { return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, evaluationContextProvider, - escapeCharacter)); + queryRewriterProvider, escapeCharacter)); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/cdi/CdiExtensionIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/cdi/CdiExtensionIntegrationTests.java index 5f0a69407..55f1a3021 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/cdi/CdiExtensionIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/cdi/CdiExtensionIntegrationTests.java @@ -24,6 +24,7 @@ import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.se.SeContainerInitializer; import jakarta.enterprise.inject.spi.Bean; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.apache.commons.logging.Log; @@ -43,7 +44,7 @@ class CdiExtensionIntegrationTests { private static Log LOGGER = LogFactory.getLog(CdiExtensionIntegrationTests.class); @BeforeAll - static void setUp() { + static void setUpCdi() { container = SeContainerInitializer.newInstance() // .disableDiscovery() // @@ -53,6 +54,11 @@ class CdiExtensionIntegrationTests { LOGGER.debug("CDI container bootstrapped!"); } + @AfterAll + static void tearDownCdi() { + container.close(); + } + @Test // DATAJPA-319, DATAJPA-1180 @SuppressWarnings("rawtypes") void foo() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/cdi/JpaQueryRewriterWithCdiIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/cdi/JpaQueryRewriterWithCdiIntegrationTests.java new file mode 100644 index 000000000..aae03b374 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/cdi/JpaQueryRewriterWithCdiIntegrationTests.java @@ -0,0 +1,234 @@ +/* + * Copyright 2008-2022 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.cdi; + +import static org.assertj.core.api.Assertions.*; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.enterprise.inject.spi.Bean; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryRewriter; +import org.springframework.data.repository.cdi.Eager; + +/** + * Unit tests for repository with {@link Query} and {@link QueryRewrite} in a CDI environment. + * + * @author Greg Turnquist + */ +public class JpaQueryRewriterWithCdiIntegrationTests { + + private static SeContainer container; + private static Log LOGGER = LogFactory.getLog(CdiExtensionIntegrationTests.class); + + private UserRepositoryWithRewriter repository; + + // Results + static final String ORIGINAL_QUERY = "original query"; + static final String REWRITTEN_QUERY = "rewritten query"; + static final String SORT = "sort"; + static Map results = new HashMap<>(); + + @BeforeAll + static void setUpCdi() { + + container = SeContainerInitializer.newInstance() // + .disableDiscovery() // + .addPackages(UserRepositoryWithRewriter.class) // + .initialize(); + + LOGGER.debug("CDI container bootstrapped!"); + } + + @AfterAll + static void tearDownCdi() { + container.close(); + } + + @BeforeEach + void setUp() { + + Bean repositoryBean = container.getBeanManager().getBeans(UserRepositoryWithRewriter.class).iterator().next(); + CreationalContext context = container.getBeanManager().createCreationalContext(repositoryBean); + this.repository = (UserRepositoryWithRewriter) container.getBeanManager().getReference(repositoryBean, + UserRepositoryWithRewriter.class, context); + + results.clear(); + } + + @Test + void nativeQueryShouldHandleRewrites() throws NoSuchMethodException { + + repository.findByNativeQuery("Matthews"); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, "select original_user_alias.* from SD_USER original_user_alias"), // + entry(REWRITTEN_QUERY, "select rewritten_user_alias.* from SD_USER rewritten_user_alias"), // + entry(SORT, Sort.unsorted().toString())); + } + + @Test + void nonNativeQueryShouldHandleRewrites() throws NoSuchMethodException { + + repository.findByNonNativeQuery("Matthews"); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, "select original_user_alias from User original_user_alias"), // + entry(REWRITTEN_QUERY, "select rewritten_user_alias from User rewritten_user_alias"), // + entry(SORT, Sort.unsorted().toString())); + } + + @Test + void nonNativeQueryWithSortShouldHandleRewrites() throws NoSuchMethodException { + + repository.findByNonNativeSortedQuery("Matthews", Sort.by("lastname")); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, + "select original_user_alias from User original_user_alias order by original_user_alias.lastname asc"), // + entry(REWRITTEN_QUERY, + "select rewritten_user_alias from User rewritten_user_alias order by rewritten_user_alias.lastname asc"), // + entry(SORT, Sort.by("lastname").ascending().toString())); + + repository.findByNonNativeSortedQuery("Matthews", Sort.by("firstname").descending()); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, + "select original_user_alias from User original_user_alias order by original_user_alias.firstname desc"), // + entry(REWRITTEN_QUERY, + "select rewritten_user_alias from User rewritten_user_alias order by rewritten_user_alias.firstname desc"), // + entry(SORT, Sort.by("firstname").descending().toString())); + } + + @Test + void nonNativeQueryWithPageableShouldHandleRewrites() throws NoSuchMethodException { + + repository.findByNonNativePagedQuery("Matthews", PageRequest.of(2, 1)); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, "select original_user_alias from User original_user_alias"), // + entry(REWRITTEN_QUERY, "select rewritten_user_alias from User rewritten_user_alias"), // + entry(SORT, Sort.unsorted().toString())); + } + + @Test + void nativeQueryWithNoRewriteAnnotationShouldNotDoRewrites() throws NoSuchMethodException { + + repository.findByNativeQueryWithNoRewrite("Matthews"); + + assertThat(results).isEmpty(); + } + + @Test + void nonNativeQueryWithNoRewriteAnnotationShouldNotDoRewrites() throws NoSuchMethodException { + + repository.findByNonNativeQueryWithNoRewrite("Matthews"); + + assertThat(results).isEmpty(); + } + + @Test + void nativeQueryShouldHandleRewritesUsingRepositoryRewriter() throws NoSuchMethodException { + + repository.findByNativeQueryUsingRepository("Matthews"); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, "select original_user_alias.* from SD_USER original_user_alias"), // + entry(REWRITTEN_QUERY, "select rewritten_user_alias.* from SD_USER rewritten_user_alias"), // + entry(SORT, Sort.unsorted().toString())); + } + + /** + * {@link QueryRewriter} implemented by the repository. + */ + @Eager + public interface UserRepositoryWithRewriter extends JpaRepository, QueryRewriter { + + @Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true, + queryRewriter = TestQueryRewriter.class) + List findByNativeQuery(String param); + + @Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class) + List findByNonNativeQuery(String param); + + @Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class) + List findByNonNativeSortedQuery(String param, Sort sort); + + @Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class) + List findByNonNativePagedQuery(String param, Pageable pageable); + + @Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true) + List findByNativeQueryWithNoRewrite(String param); + + @Query(value = "select original_user_alias from User original_user_alias") + List findByNonNativeQueryWithNoRewrite(String param); + + @Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true, + queryRewriter = UserRepositoryWithRewriter.class) + List findByNativeQueryUsingRepository(String param); + + @Override + default String rewrite(String query, Sort sort) { + return replaceAlias(query, sort); + } + } + + /** + * Stand-alone {@link QueryRewriter}. + */ + static class TestQueryRewriter implements QueryRewriter { + + @Override + public String rewrite(String query, Sort sort) { + return replaceAlias(query, sort); + } + } + + /** + * One query rewriter function to rule them all! + * + * @param query + * @param sort + */ + private static String replaceAlias(String query, Sort sort) { + + String rewrittenQuery = query.replaceAll("original_user_alias", "rewritten_user_alias"); + + // Capture results for testing. + results.put(ORIGINAL_QUERY, query); + results.put(REWRITTEN_QUERY, rewrittenQuery); + results.put(SORT, sort.toString()); + + return rewrittenQuery; + } +} 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 a7254bc96..09f7789b9 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 @@ -18,19 +18,22 @@ package org.springframework.data.jpa.repository.query; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.lang.reflect.Method; -import java.util.Set; - import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.Tuple; +import java.lang.reflect.Method; +import java.util.Set; + 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; import org.springframework.data.jpa.domain.sample.Role; 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.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; @@ -43,6 +46,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; * Integration tests for {@link AbstractStringBasedJpaQuery}. * * @author Oliver Gierke + * @author Greg Turnquist * @soundtrack Henrik Freischlader Trio - Nobody Else To Blame (Openness) */ @ExtendWith(SpringExtension.class) @@ -51,6 +55,8 @@ public class AbstractStringBasedJpaQueryIntegrationTests { @PersistenceContext EntityManager em; + @Autowired BeanFactory beanFactory; + @Test // DATAJPA-885 void createsNormalQueryForJpaManagedReturnTypes() throws Exception { @@ -60,10 +66,12 @@ public class AbstractStringBasedJpaQueryIntegrationTests { when(mock.getMetamodel()).thenReturn(em.getMetamodel()); JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class); - AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, - null, QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser()); + AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, null, + QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser(), + new QueryRewriterBeanFactoryProvider(beanFactory)); - jpaQuery.createJpaQuery(method.getAnnotatedQuery(), method.getResultProcessor().getReturnedType()); + jpaQuery.createJpaQuery(method.getAnnotatedQuery(), Sort.unsorted(), null, + method.getResultProcessor().getReturnedType()); verify(mock, times(1)).createQuery(anyString()); verify(mock, times(0)).createQuery(anyString(), eq(Tuple.class)); @@ -80,7 +88,7 @@ public class AbstractStringBasedJpaQueryIntegrationTests { interface SampleRepository extends Repository { - @org.springframework.data.jpa.repository.Query("select u.roles from User u where u.emailAddress = ?1") + @Query("select u.roles from User u where u.emailAddress = ?1") Set findRolesByEmailAddress(String emailAddress); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index 187df77ea..ed71fc826 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -19,13 +19,13 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.lang.reflect.Method; -import java.util.List; - import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.metamodel.Metamodel; +import java.lang.reflect.Method; +import java.util.List; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,7 +33,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - +import org.springframework.beans.factory.BeanFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -57,6 +57,7 @@ import org.springframework.data.repository.query.RepositoryQuery; * @author Thomas Darimont * @author Jens Schauder * @author Réda Housni Alaoui + * @author Greg Turnquist */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -70,6 +71,7 @@ public class JpaQueryLookupStrategyUnitTests { @Mock NamedQueries namedQueries; @Mock Metamodel metamodel; @Mock ProjectionFactory projectionFactory; + @Mock BeanFactory beanFactory; private JpaQueryMethodFactory queryMethodFactory; @@ -87,7 +89,7 @@ public class JpaQueryLookupStrategyUnitTests { void invalidAnnotatedQueryCausesException() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); + EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT); Method method = UserRepository.class.getMethod("findByFoo", String.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -103,7 +105,7 @@ public class JpaQueryLookupStrategyUnitTests { void sholdThrowMorePreciseExceptionIfTryingToUsePaginationInNativeQueries() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); + EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT); Method method = UserRepository.class.getMethod("findByInvalidNativeQuery", String.class, Sort.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -117,7 +119,7 @@ public class JpaQueryLookupStrategyUnitTests { void considersNamedCountQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); + EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT); when(namedQueries.hasQuery("foo.count")).thenReturn(true); when(namedQueries.getQuery("foo.count")).thenReturn("foo count"); @@ -139,7 +141,7 @@ public class JpaQueryLookupStrategyUnitTests { void considersNamedCountOnStringQueryQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); + EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT); when(namedQueries.hasQuery("foo.count")).thenReturn(true); when(namedQueries.getQuery("foo.count")).thenReturn("foo count"); @@ -158,7 +160,7 @@ public class JpaQueryLookupStrategyUnitTests { void prefersDeclaredQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); + EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT); Method method = UserRepository.class.getMethod("annotatedQueryWithQueryAndQueryName"); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java new file mode 100644 index 000000000..f11d645fc --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java @@ -0,0 +1,218 @@ +/* + * Copyright 2008-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.ImportResource; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.JpaRepository; +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.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Unit tests for repository with {@link Query} and {@link QueryRewrite}. + * + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +public class JpaQueryRewriteIntegrationTests { + + @Autowired private UserRepositoryWithRewriter repository; + + // Results + static final String ORIGINAL_QUERY = "original query"; + static final String REWRITTEN_QUERY = "rewritten query"; + static final String SORT = "sort"; + static Map results = new HashMap<>(); + + @BeforeEach + void setUp() { + results.clear(); + } + + @Test + void nativeQueryShouldHandleRewrites() throws NoSuchMethodException { + + repository.findByNativeQuery("Matthews"); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, "select original_user_alias.* from SD_USER original_user_alias"), // + entry(REWRITTEN_QUERY, "select rewritten_user_alias.* from SD_USER rewritten_user_alias"), // + entry(SORT, Sort.unsorted().toString())); + } + + @Test + void nonNativeQueryShouldHandleRewrites() throws NoSuchMethodException { + + repository.findByNonNativeQuery("Matthews"); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, "select original_user_alias from User original_user_alias"), // + entry(REWRITTEN_QUERY, "select rewritten_user_alias from User rewritten_user_alias"), // + entry(SORT, Sort.unsorted().toString())); + } + + @Test + void nonNativeQueryWithSortShouldHandleRewrites() throws NoSuchMethodException { + + repository.findByNonNativeSortedQuery("Matthews", Sort.by("lastname")); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, + "select original_user_alias from User original_user_alias order by original_user_alias.lastname asc"), // + entry(REWRITTEN_QUERY, + "select rewritten_user_alias from User rewritten_user_alias order by rewritten_user_alias.lastname asc"), // + entry(SORT, Sort.by("lastname").ascending().toString())); + + repository.findByNonNativeSortedQuery("Matthews", Sort.by("firstname").descending()); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, + "select original_user_alias from User original_user_alias order by original_user_alias.firstname desc"), // + entry(REWRITTEN_QUERY, + "select rewritten_user_alias from User rewritten_user_alias order by rewritten_user_alias.firstname desc"), // + entry(SORT, Sort.by("firstname").descending().toString())); + } + + @Test + void nonNativeQueryWithPageableShouldHandleRewrites() throws NoSuchMethodException { + + repository.findByNonNativePagedQuery("Matthews", PageRequest.of(2, 1)); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, "select original_user_alias from User original_user_alias"), // + entry(REWRITTEN_QUERY, "select rewritten_user_alias from User rewritten_user_alias"), // + entry(SORT, Sort.unsorted().toString())); + } + + @Test + void nativeQueryWithNoRewriteAnnotationShouldNotDoRewrites() throws NoSuchMethodException { + + repository.findByNativeQueryWithNoRewrite("Matthews"); + + assertThat(results).isEmpty(); + } + + @Test + void nonNativeQueryWithNoRewriteAnnotationShouldNotDoRewrites() throws NoSuchMethodException { + + repository.findByNonNativeQueryWithNoRewrite("Matthews"); + + assertThat(results).isEmpty(); + } + + @Test + void nativeQueryShouldHandleRewritesUsingRepositoryRewriter() throws NoSuchMethodException { + + repository.findByNativeQueryUsingRepository("Matthews"); + + assertThat(results).containsExactly( // + entry(ORIGINAL_QUERY, "select original_user_alias.* from SD_USER original_user_alias"), // + entry(REWRITTEN_QUERY, "select rewritten_user_alias.* from SD_USER rewritten_user_alias"), // + entry(SORT, Sort.unsorted().toString())); + } + + public interface UserRepositoryWithRewriter extends JpaRepository, QueryRewriter { + + @Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true, + queryRewriter = TestQueryRewriter.class) + List findByNativeQuery(String param); + + @Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class) + List findByNonNativeQuery(String param); + + @Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class) + List findByNonNativeSortedQuery(String param, Sort sort); + + @Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class) + List findByNonNativePagedQuery(String param, Pageable pageable); + + @Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true) + List findByNativeQueryWithNoRewrite(String param); + + @Query(value = "select original_user_alias from User original_user_alias") + List findByNonNativeQueryWithNoRewrite(String param); + + @Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true, + queryRewriter = UserRepositoryWithRewriter.class) + List findByNativeQueryUsingRepository(String param); + + @Override + default String rewrite(String query, Sort sort) { + return replaceAlias(query, sort); + } + } + + static class TestQueryRewriter implements QueryRewriter { + + @Override + public String rewrite(String query, Sort sort) { + return replaceAlias(query, sort); + } + } + + /** + * One query rewriter function to rule them all! + * + * @param query + * @param sort + */ + private static String replaceAlias(String query, Sort sort) { + + String rewrittenQuery = query.replaceAll("original_user_alias", "rewritten_user_alias"); + + // Capture results for testing. + results.put(ORIGINAL_QUERY, query); + results.put(REWRITTEN_QUERY, rewrittenQuery); + results.put(SORT, sort.toString()); + + return rewrittenQuery; + } + + @Configuration + @ImportResource("classpath:infrastructure.xml") + @EnableJpaRepositories(considerNestedRepositories = true, basePackageClasses = UserRepositoryWithRewriter.class, // + includeFilters = @ComponentScan.Filter(value = { UserRepositoryWithRewriter.class }, + type = FilterType.ASSIGNABLE_TYPE)) + static class JpaRepositoryConfig { + + @Bean + QueryRewriter queryRewriter() { + return new TestQueryRewriter(); + } + + } +} 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 077b0474f..d394bfc5c 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 @@ -19,16 +19,16 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.List; - import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; import jakarta.persistence.metamodel.Metamodel; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,7 +37,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - +import org.springframework.beans.factory.BeanFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -62,6 +62,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; * @author Jens Schauder * @author Tom Hombergs * @author Mark Paluch + * @author Greg Turnquist */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -81,6 +82,7 @@ class SimpleJpaQueryUnitTests { @Mock RepositoryMetadata metadata; @Mock ParameterBinder binder; @Mock Metamodel metamodel; + @Mock BeanFactory beanFactory; private ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); @@ -107,12 +109,12 @@ class SimpleJpaQueryUnitTests { void prefersDeclaredCountQueryOverCreatingOne() throws Exception { method = new JpaQueryMethod( - SimpleJpaQueryUnitTests.class.getDeclaredMethod("prefersDeclaredCountQueryOverCreatingOne"), - metadata, factory, extractor); + SimpleJpaQueryUnitTests.class.getDeclaredMethod("prefersDeclaredCountQueryOverCreatingOne"), metadata, factory, + extractor); when(em.createQuery("foo", Long.class)).thenReturn(typedQuery); SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, EVALUATION_CONTEXT_PROVIDER, - PARSER); + PARSER, new QueryRewriterBeanFactoryProvider(beanFactory)); assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {}))) .isEqualTo(typedQuery); @@ -127,8 +129,7 @@ class SimpleJpaQueryUnitTests { JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null, - EVALUATION_CONTEXT_PROVIDER, - PARSER); + EVALUATION_CONTEXT_PROVIDER, PARSER, new QueryRewriterBeanFactoryProvider(beanFactory)); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -143,7 +144,7 @@ class SimpleJpaQueryUnitTests { 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, EVALUATION_CONTEXT_PROVIDER); + queryMethod.getAnnotatedQuery(), null, EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory)); assertThat(jpaQuery instanceof NativeJpaQuery).isTrue(); @@ -246,8 +247,7 @@ class SimpleJpaQueryUnitTests { 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", EVALUATION_CONTEXT_PROVIDER, - PARSER); + "select count(u.id) from #{#entityName} u", EVALUATION_CONTEXT_PROVIDER, PARSER, new QueryRewriterBeanFactoryProvider(beanFactory)); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -259,7 +259,7 @@ class SimpleJpaQueryUnitTests { JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery(), null, - EVALUATION_CONTEXT_PROVIDER); + EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory)); } interface SampleRepository { diff --git a/src/main/asciidoc/jpa.adoc b/src/main/asciidoc/jpa.adoc index 7fd3d8bfa..664f72e61 100644 --- a/src/main/asciidoc/jpa.adoc +++ b/src/main/asciidoc/jpa.adoc @@ -293,7 +293,7 @@ public class User { [[jpa.query-methods.named-queries.declaring-interfaces]] ==== Declaring Interfaces -To allow these named queries, specify the `UserRepository` as follows: +To allow these named queries, specify the `UserRepositoryWithRewriter` as follows: .Query method declaration in UserRepository ====