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.
This commit is contained in:
Greg L. Turnquist
2022-03-22 09:56:05 -05:00
parent a70fff4515
commit 264472ba0f
21 changed files with 964 additions and 93 deletions

View File

@@ -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<? extends QueryRewriter> queryRewriter() default QueryRewriter.NoopQueryRewriter.class;
}

View File

@@ -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.<br/>
* <br/>
* 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;
}
}
}

View File

@@ -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<T> extends CdiRepositoryBean<T> {
private final Bean<EntityManager> entityManagerBean;
private final QueryRewriterProvider queryRewriterProvider;
/**
* Constructs a {@link JpaRepositoryBean}.
@@ -58,6 +62,7 @@ class JpaRepositoryBean<T> extends CdiRepositoryBean<T> {
Assert.notNull(entityManagerBean, "EntityManager bean must not be null!");
this.entityManagerBean = entityManagerBean;
this.queryRewriterProvider = new QueryRewriterBeanManagerProvider(beanManager);
}
@Override
@@ -65,6 +70,12 @@ class JpaRepositoryBean<T> extends CdiRepositoryBean<T> {
EntityManager entityManager = getDependencyInstance(entityManagerBean, EntityManager.class);
return create(() -> new JpaRepositoryFactory(entityManager), repositoryType);
Supplier<RepositoryFactorySupport> repositoryFactorySupportSupplier = () -> {
JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);
jpaRepositoryFactory.setQueryRewriterProvider(queryRewriterProvider);
return jpaRepositoryFactory;
};
return create(repositoryFactorySupportSupplier, repositoryType);
}
}

View File

@@ -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<? extends QueryRewriter> queryRewriter) {
try {
Bean<QueryRewriter> bean = (Bean<QueryRewriter>) beanManager.getBeans(queryRewriter).iterator().next();
CreationalContext<QueryRewriter> context = beanManager.createCreationalContext(bean);
return (QueryRewriter) beanManager.getReference(bean, queryRewriter, context);
} catch (Exception e) {
LOGGER.error(e.toString());
return null;
}
}
}

View File

@@ -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<QueryRewriter> 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);
}
}

View File

@@ -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);
}
/**

View File

@@ -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));
}

View File

@@ -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<? extends QueryRewriter> getQueryRewriter() {
return getMergedOrDefaultAnnotationValue("queryRewriter", Query.class, Class.class);
}
}

View File

@@ -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

View File

@@ -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<? extends QueryRewriter> queryRewriter) {
try {
return beanFactory.getBean(queryRewriter);
} catch (BeansException e) {
LOGGER.error(e.toString());
return null;
}
}
}

View File

@@ -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<? extends QueryRewriter> queryRewriter) {
LOGGER.warn("You have NOT configured JpaRepositoryFactory with a QueryRewriterProvider!");
return null;
}
}

View File

@@ -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<QueryRewriter> 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<? extends QueryRewriter> 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<? extends QueryRewriter> queryRewriter);
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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() {

View File

@@ -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<String, String> 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<User, Integer>, QueryRewriter {
@Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true,
queryRewriter = TestQueryRewriter.class)
List<User> findByNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class)
List<User> findByNonNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class)
List<User> findByNonNativeSortedQuery(String param, Sort sort);
@Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class)
List<User> findByNonNativePagedQuery(String param, Pageable pageable);
@Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true)
List<User> findByNativeQueryWithNoRewrite(String param);
@Query(value = "select original_user_alias from User original_user_alias")
List<User> findByNonNativeQueryWithNoRewrite(String param);
@Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true,
queryRewriter = UserRepositoryWithRewriter.class)
List<User> 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;
}
}

View File

@@ -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<User, Integer> {
@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<Role> findRolesByEmailAddress(String emailAddress);
}
}

View File

@@ -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);

View File

@@ -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<String, String> 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<User, Integer>, QueryRewriter {
@Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true,
queryRewriter = TestQueryRewriter.class)
List<User> findByNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class)
List<User> findByNonNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class)
List<User> findByNonNativeSortedQuery(String param, Sort sort);
@Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class)
List<User> findByNonNativePagedQuery(String param, Pageable pageable);
@Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true)
List<User> findByNativeQueryWithNoRewrite(String param);
@Query(value = "select original_user_alias from User original_user_alias")
List<User> findByNonNativeQueryWithNoRewrite(String param);
@Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true,
queryRewriter = UserRepositoryWithRewriter.class)
List<User> 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();
}
}
}

View File

@@ -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 {

View File

@@ -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
====