Skip to content

Commit 05ed14f

Browse files
committed
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.
1 parent fab95d9 commit 05ed14f

22 files changed

+977
-94
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<jsqlparser>4.3</jsqlparser>
2828
<mysql-connector-java>8.0.23</mysql-connector-java>
2929
<postgresql>42.2.19</postgresql>
30-
<springdata.commons>3.0.0-SNAPSHOT</springdata.commons>
30+
<springdata.commons>3.0.0-gh-2162-SNAPSHOT</springdata.commons>
3131
<vavr>0.10.3</vavr>
3232

3333
<hibernate.groupId>org.hibernate</hibernate.groupId>

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java

+13-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* @author Oliver Gierke
3030
* @author Thomas Darimont
3131
* @author Christoph Strobl
32-
*
32+
* @author Greg Turnquist
3333
* @see Modifying
3434
*/
3535
@Retention(RetentionPolicy.RUNTIME)
@@ -45,7 +45,8 @@
4545

4646
/**
4747
* Defines a special count query that shall be used for pagination queries to lookup the total number of elements for
48-
* a page. If none is configured we will derive the count query from the original query or {@link #countProjection()} query if any.
48+
* a page. If none is configured we will derive the count query from the original query or {@link #countProjection()}
49+
* query if any.
4950
*/
5051
String countQuery() default "";
5152

@@ -70,11 +71,19 @@
7071
String name() default "";
7172

7273
/**
73-
* Returns the name of the {@link jakarta.persistence.NamedQuery} to be used to execute count queries when pagination is
74-
* used. Will default to the named query name configured suffixed by {@code .count}.
74+
* Returns the name of the {@link jakarta.persistence.NamedQuery} to be used to execute count queries when pagination
75+
* is used. Will default to the named query name configured suffixed by {@code .count}.
7576
*
7677
* @see #name()
7778
* @return
7879
*/
7980
String countName() default "";
81+
82+
/**
83+
* Define the {@link QueryRewriter} bean that should be applied to this query after the query is full assembled.
84+
*
85+
* @return
86+
* @since 3.0
87+
*/
88+
Class<? extends QueryRewriter> queryRewriter() default QueryRewriter.NoopQueryRewriter.class;
8089
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2008-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository;
17+
18+
import org.springframework.data.domain.Pageable;
19+
import org.springframework.data.domain.Sort;
20+
21+
/**
22+
* Callback to rewrite a query right before it's handed to the EntityManager.
23+
*
24+
* @author Greg Turnquist
25+
* @since 3.0
26+
*/
27+
@FunctionalInterface
28+
public interface QueryRewriter {
29+
30+
/**
31+
* The assembled query and current {@link Sort} settings are offered. This is the query right before it's handed to
32+
* the EntityManager, so everything that Spring Data and tools intends to do has been done. The user is able to make
33+
* any last minute changes.<br/>
34+
* <br/>
35+
* WARNING: No checks are performed before the transformed query is passed to the EntityManager.
36+
*
37+
* @param query - the assembled generated query, right before it's handed over to the EntityManager.
38+
* @param sort - current {@link Sort} settings provided by the method, or {@link Sort#unsorted()}} if there are none.
39+
* @return alter the query however you like.
40+
*/
41+
String rewrite(String query, Sort sort);
42+
43+
/**
44+
* This alternative is used to handle {@link Pageable}-based methods.
45+
*
46+
* @param query - the assembled generated query, right before it's handed over to the EntityManager.
47+
* @param pageRequest
48+
* @return
49+
*/
50+
default String rewrite(String query, Pageable pageRequest) {
51+
return rewrite(query, pageRequest.getSort());
52+
}
53+
54+
/**
55+
* A {@link QueryRewriter} that doesn't change the query.
56+
*/
57+
public class NoopQueryRewriter implements QueryRewriter {
58+
59+
@Override
60+
public String rewrite(String query, Sort sort) {
61+
return query;
62+
}
63+
}
64+
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/JpaRepositoryBean.java

+16-5
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@
1515
*/
1616
package org.springframework.data.jpa.repository.cdi;
1717

18-
import java.lang.annotation.Annotation;
19-
import java.util.Optional;
20-
import java.util.Set;
21-
2218
import jakarta.enterprise.context.spi.CreationalContext;
2319
import jakarta.enterprise.inject.spi.Bean;
2420
import jakarta.enterprise.inject.spi.BeanManager;
2521
import jakarta.persistence.EntityManager;
2622

23+
import java.lang.annotation.Annotation;
24+
import java.util.Optional;
25+
import java.util.Set;
26+
import java.util.function.Supplier;
27+
28+
import org.springframework.data.jpa.repository.query.QueryRewriterProvider;
2729
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
2830
import org.springframework.data.repository.cdi.CdiRepositoryBean;
2931
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
32+
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
3033
import org.springframework.util.Assert;
3134

3235
/**
@@ -41,6 +44,7 @@
4144
class JpaRepositoryBean<T> extends CdiRepositoryBean<T> {
4245

4346
private final Bean<EntityManager> entityManagerBean;
47+
private final QueryRewriterProvider queryRewriterProvider;
4448

4549
/**
4650
* Constructs a {@link JpaRepositoryBean}.
@@ -58,13 +62,20 @@ class JpaRepositoryBean<T> extends CdiRepositoryBean<T> {
5862

5963
Assert.notNull(entityManagerBean, "EntityManager bean must not be null!");
6064
this.entityManagerBean = entityManagerBean;
65+
this.queryRewriterProvider = new QueryRewriterBeanManagerProvider(beanManager);
6166
}
6267

6368
@Override
6469
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType) {
6570

6671
EntityManager entityManager = getDependencyInstance(entityManagerBean, EntityManager.class);
6772

68-
return create(() -> new JpaRepositoryFactory(entityManager), repositoryType);
73+
Supplier<RepositoryFactorySupport> repositoryFactorySupportSupplier = () -> {
74+
JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);
75+
jpaRepositoryFactory.setQueryRewriterProvider(queryRewriterProvider);
76+
return jpaRepositoryFactory;
77+
};
78+
79+
return create(repositoryFactorySupportSupplier, repositoryType);
6980
}
7081
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2008-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.cdi;
17+
18+
import jakarta.enterprise.context.spi.CreationalContext;
19+
import jakarta.enterprise.inject.spi.Bean;
20+
import jakarta.enterprise.inject.spi.BeanManager;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import org.springframework.data.jpa.repository.QueryRewriter;
25+
import org.springframework.data.jpa.repository.query.QueryRewriterProvider;
26+
27+
/**
28+
* A {@link BeanManager}-based {@link QueryRewriterProvider}.
29+
*
30+
* @author Greg Turnquist
31+
* @since 3.0
32+
*/
33+
public class QueryRewriterBeanManagerProvider extends QueryRewriterProvider {
34+
35+
private static final Log LOGGER = LogFactory.getLog(QueryRewriterBeanManagerProvider.class);
36+
37+
private final BeanManager beanManager;
38+
39+
public QueryRewriterBeanManagerProvider(BeanManager beanManager) {
40+
this.beanManager = beanManager;
41+
}
42+
43+
@Override
44+
protected QueryRewriter extractQueryRewriterBean(Class<? extends QueryRewriter> queryRewriter) {
45+
46+
try {
47+
Bean<QueryRewriter> bean = (Bean<QueryRewriter>) beanManager.getBeans(queryRewriter).iterator().next();
48+
CreationalContext<QueryRewriter> context = beanManager.createCreationalContext(bean);
49+
return (QueryRewriter) beanManager.getReference(bean, queryRewriter, context);
50+
} catch (Exception e) {
51+
LOGGER.error(e.toString());
52+
return null;
53+
}
54+
}
55+
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java

+68-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
import jakarta.persistence.EntityManager;
1919
import jakarta.persistence.Query;
2020

21+
import java.util.function.Supplier;
22+
23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
25+
import org.springframework.data.domain.Pageable;
26+
import org.springframework.data.domain.Sort;
27+
import org.springframework.data.jpa.repository.QueryRewriter;
2128
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2229
import org.springframework.data.repository.query.ResultProcessor;
2330
import org.springframework.data.repository.query.ReturnedType;
@@ -35,14 +42,18 @@
3542
* @author David Madden
3643
* @author Mark Paluch
3744
* @author Diego Krupitza
45+
* @author Greg Turnquist
3846
*/
3947
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
4048

49+
private static final Log LOGGER = LogFactory.getLog(AbstractStringBasedJpaQuery.class);
50+
4151
private final DeclaredQuery query;
4252
private final DeclaredQuery countQuery;
4353
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
4454
private final SpelExpressionParser parser;
4555
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
56+
private final Supplier<QueryRewriter> queryRewriterSupplier;
4657

4758
/**
4859
* Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
@@ -57,7 +68,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
5768
*/
5869
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
5970
@Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
60-
SpelExpressionParser parser) {
71+
SpelExpressionParser parser, QueryRewriterProvider queryRewriterProvider) {
6172

6273
super(method, em);
6374

@@ -74,6 +85,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
7485
method.isNativeQuery());
7586

7687
this.parser = parser;
88+
this.queryRewriterSupplier = queryRewriterProvider.of(method);
7789

7890
Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(),
7991
"JDBC style parameters (?) are not supported for JPA queries.");
@@ -86,7 +98,8 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
8698
.applySorting(accessor.getSort(), query.getAlias());
8799
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
88100

89-
Query query = createJpaQuery(sortedQueryString, processor.getReturnedType());
101+
Query query = createJpaQuery(sortedQueryString, accessor.getSort(), accessor.getPageable(),
102+
processor.getReturnedType());
90103

91104
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query);
92105

@@ -137,18 +150,67 @@ public DeclaredQuery getCountQuery() {
137150
* Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery}
138151
* type.
139152
*/
140-
protected Query createJpaQuery(String queryString, ReturnedType returnedType) {
153+
protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable,
154+
ReturnedType returnedType) {
141155

142156
EntityManager em = getEntityManager();
143157

144158
if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) {
145-
return em.createQuery(queryString);
159+
return em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable));
146160
}
147161

148162
Class<?> typeToRead = getTypeToRead(returnedType);
149163

150164
return typeToRead == null //
151-
? em.createQuery(queryString) //
152-
: em.createQuery(queryString, typeToRead);
165+
? em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable)) //
166+
: em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable), typeToRead);
167+
}
168+
169+
// /**
170+
// * Using the {@link org.springframework.data.jpa.repository.QueryRewrite} annotation, look for a {@link
171+
// QueryRewriter}
172+
// * and instantiate one. NOTE: If its {@link QueryRewriter.NoopQueryRewriter}, it
173+
// * will just return {@literal null} and NOT do any rewrite operations.
174+
// *
175+
// * @param method - {@link JpaQueryMethod} that has the annotation details
176+
// * @return a {@link QueryRewriter for the method or {@code null}
177+
// */
178+
// @Nullable
179+
// private QueryRewriter findQueryRewriter(JpaQueryMethod method) {
180+
//
181+
// Class<? extends QueryRewriter> queryRewriter = method.getQueryRewriter();
182+
//
183+
// if (queryRewriter == null || queryRewriter == QueryRewriter.NoopQueryRewriter.class) {
184+
// return null;
185+
// }
186+
//
187+
// try {
188+
// return beanFactory.getBean(queryRewriter);
189+
// } catch (BeansException e) {
190+
// LOGGER.error(e.toString());
191+
// return null;
192+
// }
193+
// }
194+
195+
/**
196+
* Use the {@link QueryRewriter}, potentially rewrite the query, using relevant {@link Sort} and {@link Pageable}
197+
* information.
198+
*
199+
* @param originalQuery
200+
* @param sort
201+
* @param pageable
202+
* @return
203+
*/
204+
protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nullable Pageable pageable) {
205+
206+
QueryRewriter queryRewriter = this.queryRewriterSupplier.get();
207+
208+
if (queryRewriter == null) {
209+
return originalQuery;
210+
}
211+
212+
return pageable != null && pageable.isPaged() //
213+
? queryRewriter.rewrite(originalQuery, pageable) //
214+
: queryRewriter.rewrite(originalQuery, sort);
153215
}
154216
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ enum JpaQueryFactory {
4545
* @return
4646
*/
4747
AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString,
48-
@Nullable String countQueryString,
49-
QueryMethodEvaluationContextProvider evaluationContextProvider) {
48+
@Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
49+
QueryRewriterProvider queryRewriterProvider) {
5050

5151
return method.isNativeQuery()
52-
? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER)
53-
: new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER);
52+
? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER,
53+
queryRewriterProvider)
54+
: new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER,
55+
queryRewriterProvider);
5456
}
5557

5658
/**

0 commit comments

Comments
 (0)