Polishing.

Eagerly resolve QueryRewriter instances when creating JPA query objects. Tweak documentation wording. Tweak type names to align with naming scheme.

See #2162.
This commit is contained in:
Mark Paluch
2022-04-29 13:39:29 +02:00
committed by Greg L. Turnquist
parent 264472ba0f
commit 6c94e0e452
20 changed files with 249 additions and 224 deletions

View File

@@ -80,10 +80,10 @@ public @interface Query {
String countName() default "";
/**
* Define the {@link QueryRewriter} bean that should be applied to this query after the query is full assembled.
* Define a {@link QueryRewriter} that should be applied to the query string after the query is fully assembled.
*
* @return
* @since 3.0
*/
Class<? extends QueryRewriter> queryRewriter() default QueryRewriter.NoopQueryRewriter.class;
Class<? extends QueryRewriter> queryRewriter() default QueryRewriter.IdentityQueryRewriter.class;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2022 the original author or authors.
* Copyright 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.
@@ -19,33 +19,41 @@ 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.
* Callback to rewrite a query and apply sorting and pagination settings that cannot be applied based on a regularly
* detectable scheme.
* <p>
* The underlying the query is the one right before it is used for query object creation, so everything that Spring Data
* and tools intends to do has been done. You can customize the query to apply final changes. Rewriting can only make
* use of already existing contextual data. That is, adding or replacing query text or reuse of bound parameters. Query
* rewriting must not add additional bindable parameters as these cannot be materialized.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.0
* @see jakarta.persistence.EntityManager#createQuery
* @see jakarta.persistence.EntityManager#createNativeQuery
*/
@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/>
* Rewrite the assembled query with the given {@link Sort}.
* <p>
* 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.
*
* @param query the assembled query.
* @param sort current {@link Sort} settings provided by the method, or {@link Sort#unsorted()}} if there are none.
* @return the query to be used with the {@code EntityManager}.
*/
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
* Rewrite the assembled query with the given {@link Pageable}.
*
* @param query the assembled query.
* @param pageRequest current {@link Pageable} settings provided by the method, or {@link Pageable#unpaged()} if not
* paged.
* @return the query to be used with the {@code EntityManager}.
*/
default String rewrite(String query, Pageable pageRequest) {
return rewrite(query, pageRequest.getSort());
@@ -54,7 +62,9 @@ public interface QueryRewriter {
/**
* A {@link QueryRewriter} that doesn't change the query.
*/
public class NoopQueryRewriter implements QueryRewriter {
enum IdentityQueryRewriter implements QueryRewriter {
INSTANCE;
@Override
public String rewrite(String query, Sort sort) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2022 the original author or authors.
* Copyright 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.
@@ -19,37 +19,52 @@ 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 java.util.Iterator;
import org.springframework.beans.BeanUtils;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.jpa.repository.query.DelegatingQueryRewriter;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.query.QueryRewriterProvider;
import org.springframework.data.util.Lazy;
/**
* A {@link BeanManager}-based {@link QueryRewriterProvider}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.0
*/
public class QueryRewriterBeanManagerProvider extends QueryRewriterProvider {
private static final Log LOGGER = LogFactory.getLog(QueryRewriterBeanManagerProvider.class);
public class BeanManagerQueryRewriterProvider implements QueryRewriterProvider {
private final BeanManager beanManager;
public QueryRewriterBeanManagerProvider(BeanManager beanManager) {
public BeanManagerQueryRewriterProvider(BeanManager beanManager) {
this.beanManager = beanManager;
}
@Override
protected QueryRewriter extractQueryRewriterBean(Class<? extends QueryRewriter> queryRewriter) {
@SuppressWarnings("unchecked")
public QueryRewriter getQueryRewriter(JpaQueryMethod method) {
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;
Class<? extends QueryRewriter> queryRewriter = method.getQueryRewriter();
if (queryRewriter == QueryRewriter.IdentityQueryRewriter.class) {
return QueryRewriter.IdentityQueryRewriter.INSTANCE;
}
Iterator<Bean<?>> iterator = beanManager.getBeans(queryRewriter).iterator();
if (iterator.hasNext()) {
Bean<QueryRewriter> bean = (Bean<QueryRewriter>) iterator.next();
CreationalContext<QueryRewriter> context = beanManager.createCreationalContext(bean);
Lazy<QueryRewriter> rewriter = Lazy
.of(() -> (QueryRewriter) beanManager.getReference(bean, queryRewriter, context));
return new DelegatingQueryRewriter(rewriter);
}
return BeanUtils.instantiateClass(queryRewriter);
}
}

View File

@@ -62,7 +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);
this.queryRewriterProvider = new BeanManagerQueryRewriterProvider(beanManager);
}
@Override

View File

@@ -18,10 +18,9 @@ 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;
@@ -46,14 +45,12 @@ import org.springframework.util.Assert;
*/
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;
private final QueryRewriter queryRewriter;
/**
* Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
@@ -65,16 +62,18 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* @param countQueryString must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @param parser must not be {@literal null}.
* @param queryRewriter must not be {@literal null}.
*/
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
@Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
SpelExpressionParser parser, QueryRewriterProvider queryRewriterProvider) {
@Nullable String countQueryString, QueryRewriter queryRewriter, QueryMethodEvaluationContextProvider evaluationContextProvider,
SpelExpressionParser parser) {
super(method, em);
Assert.hasText(queryString, "Query string must not be null or empty!");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null!");
Assert.notNull(parser, "Parser must not be null!");
Assert.notNull(queryRewriter, "QueryRewriter must not be null!");
this.evaluationContextProvider = evaluationContextProvider;
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser,
@@ -85,7 +84,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
method.isNativeQuery());
this.parser = parser;
this.queryRewriterSupplier = queryRewriterProvider.of(method);
this.queryRewriter = queryRewriter;
Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(),
"JDBC style parameters (?) are not supported for JPA queries.");
@@ -169,7 +168,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
/**
* Use the {@link QueryRewriter}, potentially rewrite the query, using relevant {@link Sort} and {@link Pageable}
* information.
*
*
* @param originalQuery
* @param sort
* @param pageable
@@ -177,12 +176,6 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
*/
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

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2022 the original author or authors.
* Copyright 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.
@@ -15,36 +15,38 @@
*/
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.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.util.Lazy;
/**
* A {@link BeanFactory}-based {@link QueryRewriterProvider}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.0
*/
public class QueryRewriterBeanFactoryProvider extends QueryRewriterProvider {
private static final Log LOGGER = LogFactory.getLog(QueryRewriterBeanFactoryProvider.class);
public class BeanFactoryQueryRewriterProvider implements QueryRewriterProvider {
private final BeanFactory beanFactory;
public QueryRewriterBeanFactoryProvider(BeanFactory beanFactory) {
public BeanFactoryQueryRewriterProvider(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
protected QueryRewriter extractQueryRewriterBean(Class<? extends QueryRewriter> queryRewriter) {
@SuppressWarnings("unchecked")
public QueryRewriter getQueryRewriter(JpaQueryMethod method) {
try {
return beanFactory.getBean(queryRewriter);
} catch (BeansException e) {
LOGGER.error(e.toString());
return null;
Class<? extends QueryRewriter> queryRewriter = method.getQueryRewriter();
if (queryRewriter == QueryRewriter.IdentityQueryRewriter.class) {
return QueryRewriter.IdentityQueryRewriter.INSTANCE;
}
Lazy<QueryRewriter> rewriter = Lazy.of(() -> beanFactory.getBeanProvider((Class<QueryRewriter>) queryRewriter)
.getIfAvailable(() -> BeanUtils.instantiateClass(queryRewriter)));
return new DelegatingQueryRewriter(rewriter);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 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.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.QueryRewriter;
/**
* Delegating {@link QueryRewriter} that delegates rewrite calls to a {@link QueryRewriter delegate} provided by a
* {@link Supplier}.
*
* @author Mark Paluch
* @since 3.0
*/
public class DelegatingQueryRewriter implements QueryRewriter {
private final Supplier<QueryRewriter> delegate;
public DelegatingQueryRewriter(Supplier<QueryRewriter> delegate) {
this.delegate = delegate;
}
@Override
public String rewrite(String query, Sort sort) {
return delegate.get().rewrite(query, sort);
}
@Override
public String rewrite(String query, Pageable pageRequest) {
return delegate.get().rewrite(query, pageRequest);
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.query;
import jakarta.persistence.EntityManager;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -39,20 +40,20 @@ enum JpaQueryFactory {
*
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null} or empty.
* @param countQueryString
* @param queryString must not be {@literal null}.
* @param evaluationContextProvider
* @return
*/
AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString,
@Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
QueryRewriterProvider queryRewriterProvider) {
@Nullable String countQueryString, QueryRewriter queryRewriter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return method.isNativeQuery()
? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER,
queryRewriterProvider)
: new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER,
queryRewriterProvider);
? new NativeJpaQuery(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider,
PARSER)
: new SimpleJpaQuery(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider,
PARSER);
}
/**

View File

@@ -22,6 +22,7 @@ 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.jpa.repository.QueryRewriter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -83,14 +84,13 @@ public final class JpaQueryLookupStrategy {
@Override
public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
return resolveQuery(queryMethodFactory.build(method, metadata, factory), em, namedQueries);
JpaQueryMethod queryMethod = queryMethodFactory.build(method, metadata, factory);
return resolveQuery(queryMethod, queryRewriterProvider.getQueryRewriter(queryMethod), em, namedQueries);
}
protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries);
protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter,
EntityManager em, NamedQueries namedQueries);
protected QueryRewriterProvider getQueryRewriterSupplier() {
return queryRewriterProvider;
}
}
/**
@@ -112,7 +112,8 @@ public final class JpaQueryLookupStrategy {
}
@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em,
NamedQueries namedQueries) {
return new PartTreeJpaQuery(method, em, escape);
}
}
@@ -145,7 +146,8 @@ public final class JpaQueryLookupStrategy {
}
@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em,
NamedQueries namedQueries) {
if (method.isProcedureQuery()) {
return JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em);
@@ -159,13 +161,13 @@ public final class JpaQueryLookupStrategy {
}
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getRequiredAnnotatedQuery(),
getCountQuery(method, namedQueries, em), evaluationContextProvider, getQueryRewriterSupplier());
getCountQuery(method, namedQueries, em), queryRewriter, evaluationContextProvider);
}
String name = method.getNamedQueryName();
if (namedQueries.hasQuery(name)) {
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name),
getCountQuery(method, namedQueries, em), evaluationContextProvider, getQueryRewriterSupplier());
getCountQuery(method, namedQueries, em), queryRewriter, evaluationContextProvider);
}
RepositoryQuery query = NamedQuery.lookupFrom(method, em);
@@ -240,12 +242,13 @@ public final class JpaQueryLookupStrategy {
}
@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em,
NamedQueries namedQueries) {
try {
return lookupStrategy.resolveQuery(method, em, namedQueries);
return lookupStrategy.resolveQuery(method, queryRewriter, em, namedQueries);
} catch (IllegalStateException e) {
return createStrategy.resolveQuery(method, em, namedQueries);
return createStrategy.resolveQuery(method, queryRewriter, em, namedQueries);
}
}
}
@@ -270,7 +273,8 @@ public final class JpaQueryLookupStrategy {
case CREATE:
return new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape);
case USE_DECLARED_QUERY:
return new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider, queryRewriterProvider);
return new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider,
queryRewriterProvider);
case CREATE_IF_NOT_FOUND:
return new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory,
new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape),

View File

@@ -266,7 +266,7 @@ public class JpaQueryMethod extends QueryMethod {
* @return
*/
@Nullable
String getAnnotatedQuery() {
public String getAnnotatedQuery() {
String query = getAnnotationValue("value", String.class);
return StringUtils.hasText(query) ? query : null;
@@ -287,7 +287,7 @@ public class JpaQueryMethod extends QueryMethod {
* @throws IllegalStateException if no {@link Query} annotation is present or the query is empty.
* @since 2.0
*/
String getRequiredAnnotatedQuery() throws IllegalStateException {
public String getRequiredAnnotatedQuery() throws IllegalStateException {
String query = getAnnotatedQuery();
@@ -305,7 +305,7 @@ public class JpaQueryMethod extends QueryMethod {
* @return
*/
@Nullable
String getCountQuery() {
public String getCountQuery() {
String countQuery = getAnnotationValue("countQuery", String.class);
return StringUtils.hasText(countQuery) ? countQuery : null;
@@ -438,8 +438,7 @@ public class JpaQueryMethod extends QueryMethod {
* @return type of the {@link QueryRewriter}
* @since 3.0
*/
@Nullable
Class<? extends QueryRewriter> getQueryRewriter() {
public Class<? extends QueryRewriter> getQueryRewriter() {
return getMergedOrDefaultAnnotationValue("queryRewriter", Query.class, Class.class);
}
}

View File

@@ -21,6 +21,7 @@ import jakarta.persistence.Tuple;
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.Parameters;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
@@ -48,13 +49,13 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null} or empty.
* @param countQueryString must not be {@literal null} or empty.
* @param evaluationContextProvider
* @param rewriter the query rewriter to use.
*/
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser,
QueryRewriterProvider queryRewriterProvider) {
QueryRewriter rewriter, QueryMethodEvaluationContextProvider evaluationContextProvider,
SpelExpressionParser parser) {
super(method, em, queryString, countQueryString, evaluationContextProvider, parser, queryRewriterProvider);
super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser);
Parameters<?, ?> parameters = method.getParameters();

View File

@@ -1,45 +0,0 @@
/*
* 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

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2022 the original author or authors.
* Copyright 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.
@@ -15,57 +15,45 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.function.Supplier;
import org.springframework.beans.BeanUtils;
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.)
* Provide a {@link QueryRewriter} based upon the {@link JpaQueryMethod}. {@code QueryRewriter} instances may be
* contextual or plain objects that are not attached to a bean factory or CDI context.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.0
* @see QueryRewriter
*/
public abstract class QueryRewriterProvider {
public interface 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}
* Return a simple {@code QueryRewriterProvider} that uses
* {@link org.springframework.beans.BeanUtils#instantiateClass(Class)} to obtain a {@link QueryRewriter} instance.
*
* @return a simple {@link QueryRewriterProvider}.
*/
public Supplier<QueryRewriter> of(JpaQueryMethod method) {
return () -> findQueryRewriter(method);
static QueryRewriterProvider simple() {
return method -> {
Class<? extends QueryRewriter> queryRewriter = method.getQueryRewriter();
if (queryRewriter == QueryRewriter.IdentityQueryRewriter.class) {
return QueryRewriter.IdentityQueryRewriter.INSTANCE;
}
return BeanUtils.instantiateClass(queryRewriter);
};
}
/**
* 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.
* Obtain an instance of {@link QueryRewriter} for a {@link JpaQueryMethod}.
*
* @param method - {@link JpaQueryMethod} that has the annotation details
* @return a {@link QueryRewriter for the method or {@code null}
* @param method the underlying JPA query method.
* @return a Java bean that implements {@link QueryRewriter}.
*/
@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);
QueryRewriter getQueryRewriter(JpaQueryMethod method);
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -41,14 +42,13 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
* @param method must not be {@literal null}
* @param em must not be {@literal null}
* @param countQueryString
* @param queryRewriter must not be {@literal null}
* @param evaluationContextProvider must not be {@literal null}
* @param parser must not be {@literal null}
*/
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString,
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser,
QueryRewriterProvider queryRewriterProvider) {
this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, evaluationContextProvider, parser,
queryRewriterProvider);
QueryRewriter queryRewriter, QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryRewriter, evaluationContextProvider, parser);
}
/**
@@ -58,14 +58,14 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
* @param em must not be {@literal null}
* @param queryString must not be {@literal null} or empty
* @param countQueryString
* @param queryRewriter
* @param evaluationContextProvider must not be {@literal null}
* @param parser must not be {@literal null}
*/
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser,
QueryRewriterProvider queryRewriterProvider) {
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, QueryRewriter queryRewriter,
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
super(method, em, queryString, countQueryString, evaluationContextProvider, parser, queryRewriterProvider);
super(method, em, queryString, countQueryString, queryRewriter, evaluationContextProvider, parser);
validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s!", method);

View File

@@ -27,6 +27,8 @@ import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.dao.InvalidDataAccessApiUsageException;
@@ -34,7 +36,15 @@ 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.*;
import org.springframework.data.jpa.repository.query.AbstractJpaQuery;
import org.springframework.data.jpa.repository.query.BeanFactoryQueryRewriterProvider;
import org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.jpa.repository.query.QueryRewriterProvider;
import org.springframework.data.jpa.util.JpaMetamodel;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.querydsl.EntityPathResolver;
@@ -93,12 +103,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
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();
this.queryRewriterProvider = QueryRewriterProvider.simple();
addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
addRepositoryProxyPostProcessor((factory, repositoryInformation) -> {
@@ -122,8 +127,8 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
/**
* 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}.
*
* {@link QueryRewriterProvider} being a {@link BeanFactoryQueryRewriterProvider}.
*
* @param beanFactory
* @throws BeansException
*/
@@ -134,7 +139,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
Assert.notNull(beanFactory, "BeanFactory must not be null!");
setQueryRewriterProvider(new QueryRewriterBeanFactoryProvider(beanFactory));
setQueryRewriterProvider(new BeanFactoryQueryRewriterProvider(beanFactory));
}
/**
@@ -171,9 +176,11 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
}
/**
* Configures the {@link QueryRewriterProvider} to be used. Defaults to {@link QueryRewriterNoopProvider}.
* Configures the {@link QueryRewriterProvider} to be used. Defaults to instantiate query rewriters through
* {@link BeanUtils#instantiateClass(Class)}.
*
* @param queryRewriterProvider must not be {@literal null}
* @param queryRewriterProvider must not be {@literal null}.
* @since 3.0
*/
public void setQueryRewriterProvider(QueryRewriterProvider queryRewriterProvider) {
@@ -254,7 +261,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
*
* @param metadata repository metadata.
* @param entityManager the entity manager.
* @param resolver resolver to translate an plain domain class into a {@link EntityPath}.
* @param resolver resolver to translate a plain domain class into a {@link EntityPath}.
* @param crudMethodMetadata metadata about the invoked CRUD methods.
* @return
* @since 2.5.1

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2022 the original author or authors.
* Copyright 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.
@@ -43,7 +43,7 @@ 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 {

View File

@@ -34,6 +34,7 @@ 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.jpa.repository.QueryRewriter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@@ -66,9 +67,8 @@ 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(),
new QueryRewriterBeanFactoryProvider(beanFactory));
AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, null, QueryRewriter.IdentityQueryRewriter.INSTANCE,
QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser());
jpaQuery.createJpaQuery(method.getAnnotatedQuery(), Sort.unsorted(), null,
method.getResultProcessor().getReturnedType());

View File

@@ -89,7 +89,7 @@ public class JpaQueryLookupStrategyUnitTests {
void invalidAnnotatedQueryCausesException() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT);
EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
Method method = UserRepository.class.getMethod("findByFoo", String.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
@@ -105,7 +105,7 @@ public class JpaQueryLookupStrategyUnitTests {
void sholdThrowMorePreciseExceptionIfTryingToUsePaginationInNativeQueries() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT);
EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
Method method = UserRepository.class.getMethod("findByInvalidNativeQuery", String.class, Sort.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
@@ -119,7 +119,7 @@ public class JpaQueryLookupStrategyUnitTests {
void considersNamedCountQuery() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT);
EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
when(namedQueries.getQuery("foo.count")).thenReturn("foo count");
@@ -141,7 +141,7 @@ public class JpaQueryLookupStrategyUnitTests {
void considersNamedCountOnStringQueryQuery() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT);
EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
when(namedQueries.getQuery("foo.count")).thenReturn("foo count");
@@ -160,7 +160,7 @@ public class JpaQueryLookupStrategyUnitTests {
void prefersDeclaredQuery() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory), EscapeCharacter.DEFAULT);
EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
Method method = UserRepository.class.getMethod("annotatedQueryWithQueryAndQueryName");
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2022 the original author or authors.
* Copyright 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.
@@ -43,7 +43,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Unit tests for repository with {@link Query} and {@link QueryRewrite}.
*
*
* @author Greg Turnquist
*/
@ExtendWith(SpringExtension.class)
@@ -64,7 +64,7 @@ public class JpaQueryRewriteIntegrationTests {
}
@Test
void nativeQueryShouldHandleRewrites() throws NoSuchMethodException {
void nativeQueryShouldHandleRewrites() {
repository.findByNativeQuery("Matthews");
@@ -75,7 +75,7 @@ public class JpaQueryRewriteIntegrationTests {
}
@Test
void nonNativeQueryShouldHandleRewrites() throws NoSuchMethodException {
void nonNativeQueryShouldHandleRewrites() {
repository.findByNonNativeQuery("Matthews");
@@ -86,7 +86,7 @@ public class JpaQueryRewriteIntegrationTests {
}
@Test
void nonNativeQueryWithSortShouldHandleRewrites() throws NoSuchMethodException {
void nonNativeQueryWithSortShouldHandleRewrites() {
repository.findByNonNativeSortedQuery("Matthews", Sort.by("lastname"));
@@ -108,7 +108,7 @@ public class JpaQueryRewriteIntegrationTests {
}
@Test
void nonNativeQueryWithPageableShouldHandleRewrites() throws NoSuchMethodException {
void nonNativeQueryWithPageableShouldHandleRewrites() {
repository.findByNonNativePagedQuery("Matthews", PageRequest.of(2, 1));
@@ -119,7 +119,7 @@ public class JpaQueryRewriteIntegrationTests {
}
@Test
void nativeQueryWithNoRewriteAnnotationShouldNotDoRewrites() throws NoSuchMethodException {
void nativeQueryWithNoRewriteAnnotationShouldNotDoRewrites() {
repository.findByNativeQueryWithNoRewrite("Matthews");
@@ -127,7 +127,7 @@ public class JpaQueryRewriteIntegrationTests {
}
@Test
void nonNativeQueryWithNoRewriteAnnotationShouldNotDoRewrites() throws NoSuchMethodException {
void nonNativeQueryWithNoRewriteAnnotationShouldNotDoRewrites() {
repository.findByNonNativeQueryWithNoRewrite("Matthews");
@@ -135,7 +135,7 @@ public class JpaQueryRewriteIntegrationTests {
}
@Test
void nativeQueryShouldHandleRewritesUsingRepositoryRewriter() throws NoSuchMethodException {
void nativeQueryShouldHandleRewritesUsingRepositoryRewriter() {
repository.findByNativeQueryUsingRepository("Matthews");

View File

@@ -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;
@@ -45,6 +45,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@@ -82,7 +83,6 @@ class SimpleJpaQueryUnitTests {
@Mock RepositoryMetadata metadata;
@Mock ParameterBinder binder;
@Mock Metamodel metamodel;
@Mock BeanFactory beanFactory;
private ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
@@ -113,8 +113,8 @@ class SimpleJpaQueryUnitTests {
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, new QueryRewriterBeanFactoryProvider(beanFactory));
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null,
QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER, PARSER);
assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {})))
.isEqualTo(typedQuery);
@@ -129,7 +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, new QueryRewriterBeanFactoryProvider(beanFactory));
QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER, PARSER);
jpaQuery.createCountQuery(
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
@@ -144,7 +144,8 @@ 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, new QueryRewriterBeanFactoryProvider(beanFactory));
queryMethod.getAnnotatedQuery(), null, QueryRewriter.IdentityQueryRewriter.INSTANCE,
EVALUATION_CONTEXT_PROVIDER);
assertThat(jpaQuery instanceof NativeJpaQuery).isTrue();
@@ -247,7 +248,8 @@ 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, new QueryRewriterBeanFactoryProvider(beanFactory));
"select count(u.id) from #{#entityName} u", QueryRewriter.IdentityQueryRewriter.INSTANCE,
EVALUATION_CONTEXT_PROVIDER, PARSER);
jpaQuery.createCountQuery(
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
@@ -259,7 +261,7 @@ class SimpleJpaQueryUnitTests {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery(), null,
EVALUATION_CONTEXT_PROVIDER, new QueryRewriterBeanFactoryProvider(beanFactory));
QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER);
}
interface SampleRepository {