Skip to content

Commit c41917f

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 c41917f

16 files changed

+546
-81
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+
* Returns the class of 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,39 @@
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 java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* Annotation to indicate source of {@link QueryRewriter}.
26+
*
27+
* @author Greg Turnquist
28+
* @since 3.0
29+
*/
30+
@Retention(RetentionPolicy.RUNTIME)
31+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
32+
@Documented
33+
public @interface QueryRewrite {
34+
35+
/**
36+
* Define the {@link QueryRewriter} to callback.
37+
*/
38+
Class<? extends QueryRewriter> value();
39+
}
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 via {@link QueryRewrite} on a method with {@link Query}.
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 this 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()}} is 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/query/AbstractStringBasedJpaQuery.java

+71-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
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.beans.BeansException;
26+
import org.springframework.beans.factory.BeanFactory;
27+
import org.springframework.data.domain.Pageable;
28+
import org.springframework.data.domain.Sort;
29+
import org.springframework.data.jpa.repository.QueryRewriter;
2130
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2231
import org.springframework.data.repository.query.ResultProcessor;
2332
import org.springframework.data.repository.query.ReturnedType;
@@ -35,14 +44,19 @@
3544
* @author David Madden
3645
* @author Mark Paluch
3746
* @author Diego Krupitza
47+
* @author Greg Turnquist
3848
*/
3949
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
4050

51+
private static final Log LOGGER = LogFactory.getLog(AbstractStringBasedJpaQuery.class);
52+
4153
private final DeclaredQuery query;
4254
private final DeclaredQuery countQuery;
4355
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
4456
private final SpelExpressionParser parser;
4557
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
58+
private final Supplier<QueryRewriter> queryRewriterSupplier;
59+
private final BeanFactory beanFactory;
4660

4761
/**
4862
* Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
@@ -57,7 +71,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
5771
*/
5872
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
5973
@Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
60-
SpelExpressionParser parser) {
74+
SpelExpressionParser parser, BeanFactory beanFactory) {
6175

6276
super(method, em);
6377

@@ -74,6 +88,8 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
7488
method.isNativeQuery());
7589

7690
this.parser = parser;
91+
this.beanFactory = beanFactory;
92+
this.queryRewriterSupplier = () -> findQueryRewriter(method);
7793

7894
Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(),
7995
"JDBC style parameters (?) are not supported for JPA queries.");
@@ -86,7 +102,8 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
86102
.applySorting(accessor.getSort(), query.getAlias());
87103
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
88104

89-
Query query = createJpaQuery(sortedQueryString, processor.getReturnedType());
105+
Query query = createJpaQuery(sortedQueryString, accessor.getSort(), accessor.getPageable(),
106+
processor.getReturnedType());
90107

91108
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query);
92109

@@ -137,18 +154,66 @@ public DeclaredQuery getCountQuery() {
137154
* Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery}
138155
* type.
139156
*/
140-
protected Query createJpaQuery(String queryString, ReturnedType returnedType) {
157+
protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable,
158+
ReturnedType returnedType) {
141159

142160
EntityManager em = getEntityManager();
143161

144162
if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) {
145-
return em.createQuery(queryString);
163+
return em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable));
146164
}
147165

148166
Class<?> typeToRead = getTypeToRead(returnedType);
149167

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

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import jakarta.persistence.EntityManager;
1919

20+
import org.springframework.beans.factory.BeanFactory;
2021
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2122
import org.springframework.data.repository.query.RepositoryQuery;
2223
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -45,12 +46,12 @@ enum JpaQueryFactory {
4546
* @return
4647
*/
4748
AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString,
48-
@Nullable String countQueryString,
49-
QueryMethodEvaluationContextProvider evaluationContextProvider) {
49+
@Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
50+
BeanFactory beanFactory) {
5051

5152
return method.isNativeQuery()
52-
? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER)
53-
: new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER);
53+
? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER, beanFactory)
54+
: new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER, beanFactory);
5455
}
5556

5657
/**

0 commit comments

Comments
 (0)