Skip to content

Commit fddc56d

Browse files
committed
Introduce QueryRewriter.
Allow a QueryRewriter to be applied to any query crafted using @query via an additional @QueryRewriter annotation. See #2162.
1 parent fe85a33 commit fddc56d

File tree

8 files changed

+450
-20
lines changed

8 files changed

+450
-20
lines changed
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,35 @@
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+
String rewrite(String query, Sort sort);
31+
32+
default String rewrite(String query, Pageable pageRequest) {
33+
return rewrite(query, pageRequest.getSort());
34+
}
35+
}

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

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

21+
import java.lang.reflect.Constructor;
22+
import java.lang.reflect.InvocationTargetException;
23+
24+
import org.springframework.data.domain.Pageable;
25+
import org.springframework.data.domain.Sort;
26+
import org.springframework.data.jpa.repository.QueryRewriter;
2127
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2228
import org.springframework.data.repository.query.ResultProcessor;
2329
import org.springframework.data.repository.query.ReturnedType;
@@ -35,6 +41,7 @@
3541
* @author David Madden
3642
* @author Mark Paluch
3743
* @author Diego Krupitza
44+
* @author Greg Turnquist
3845
*/
3946
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
4047

@@ -43,6 +50,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
4350
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
4451
private final SpelExpressionParser parser;
4552
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
53+
private final QueryRewriter queryRewriter;
4654

4755
/**
4856
* Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
@@ -74,6 +82,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
7482
method.isNativeQuery());
7583

7684
this.parser = parser;
85+
this.queryRewriter = findQueryRewriter(method);
7786

7887
Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(),
7988
"JDBC style parameters (?) are not supported for JPA queries.");
@@ -86,7 +95,8 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
8695
.applySorting(accessor.getSort(), query.getAlias());
8796
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
8897

89-
Query query = createJpaQuery(sortedQueryString, processor.getReturnedType());
98+
Query query = createJpaQuery(sortedQueryString, accessor.getSort(), accessor.getPageable(),
99+
processor.getReturnedType());
90100

91101
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query);
92102

@@ -137,7 +147,8 @@ public DeclaredQuery getCountQuery() {
137147
* Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery}
138148
* type.
139149
*/
140-
protected Query createJpaQuery(String queryString, ReturnedType returnedType) {
150+
protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable,
151+
ReturnedType returnedType) {
141152

142153
EntityManager em = getEntityManager();
143154

@@ -148,7 +159,42 @@ protected Query createJpaQuery(String queryString, ReturnedType returnedType) {
148159
Class<?> typeToRead = getTypeToRead(returnedType);
149160

150161
return typeToRead == null //
151-
? em.createQuery(queryString) //
152-
: em.createQuery(queryString, typeToRead);
162+
? em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable)) //
163+
: em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable), typeToRead);
164+
}
165+
166+
/**
167+
* Useing the {@link org.springframework.data.jpa.repository.QueryRewrite} annotation, look for a
168+
* {@link QueryRewriter} and instantiate one.
169+
*
170+
* @param method - {@link JpaQueryMethod} that has the annotation details
171+
* @return a {@link QueryRewriter for the method or {@code null}
172+
*/
173+
@Nullable
174+
protected QueryRewriter findQueryRewriter(JpaQueryMethod method) {
175+
176+
Class<? extends QueryRewriter> queryRewriter = method.getQueryRewriter();
177+
178+
if (queryRewriter == null) {
179+
return null;
180+
}
181+
182+
try {
183+
return (QueryRewriter) ((Constructor<?>) queryRewriter.getDeclaredConstructor()).newInstance();
184+
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
185+
System.out.println(e);
186+
return null;
187+
}
188+
}
189+
190+
protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nullable Pageable pageable) {
191+
192+
if (this.queryRewriter == null) {
193+
return originalQuery;
194+
}
195+
196+
return pageable != null && pageable.isPaged() //
197+
? this.queryRewriter.rewrite(originalQuery, pageable) //
198+
: this.queryRewriter.rewrite(originalQuery, sort);
153199
}
154200
}

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

+16-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18+
import jakarta.persistence.LockModeType;
19+
import jakarta.persistence.QueryHint;
20+
1821
import java.lang.annotation.Annotation;
1922
import java.lang.reflect.Method;
2023
import java.util.Arrays;
@@ -24,9 +27,6 @@
2427
import java.util.Optional;
2528
import java.util.Set;
2629

27-
import jakarta.persistence.LockModeType;
28-
import jakarta.persistence.QueryHint;
29-
3030
import org.springframework.core.annotation.AnnotatedElementUtils;
3131
import org.springframework.core.annotation.AnnotationUtils;
3232
import org.springframework.data.jpa.provider.QueryExtractor;
@@ -35,6 +35,8 @@
3535
import org.springframework.data.jpa.repository.Modifying;
3636
import org.springframework.data.jpa.repository.Query;
3737
import org.springframework.data.jpa.repository.QueryHints;
38+
import org.springframework.data.jpa.repository.QueryRewrite;
39+
import org.springframework.data.jpa.repository.QueryRewriter;
3840
import org.springframework.data.projection.ProjectionFactory;
3941
import org.springframework.data.repository.core.RepositoryMetadata;
4042
import org.springframework.data.repository.query.Parameter;
@@ -57,6 +59,7 @@
5759
* @author Mark Paluch
5860
* @author Сергей Цыпанов
5961
* @author Réda Housni Alaoui
62+
* @author Greg Turnquist
6063
*/
6164
public class JpaQueryMethod extends QueryMethod {
6265

@@ -430,4 +433,14 @@ StoredProcedureAttributes getProcedureAttributes() {
430433
return storedProcedureAttributes;
431434
}
432435

436+
/**
437+
* Returns the {@link QueryRewriter} (if there is one). NOTE: This is only used for native (@Query) methods.
438+
*
439+
* @return
440+
* @since 3.0
441+
*/
442+
@Nullable
443+
Class<? extends QueryRewriter> getQueryRewriter() {
444+
return getMergedOrDefaultAnnotationValue("value", QueryRewrite.class, Class.class);
445+
}
433446
}

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import jakarta.persistence.Query;
2020
import jakarta.persistence.Tuple;
2121

22+
import org.springframework.data.domain.Pageable;
23+
import org.springframework.data.domain.Sort;
2224
import org.springframework.data.repository.query.Parameters;
2325
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2426
import org.springframework.data.repository.query.RepositoryQuery;
@@ -35,6 +37,7 @@
3537
* @author Oliver Gierke
3638
* @author Jens Schauder
3739
* @author Mark Paluch
40+
* @author Greg Turnquist
3841
*/
3942
final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
4043

@@ -60,12 +63,13 @@ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryStrin
6063
}
6164

6265
@Override
63-
protected Query createJpaQuery(String queryString, ReturnedType returnedType) {
66+
protected Query createJpaQuery(String queryString, Sort sort, Pageable pageable, ReturnedType returnedType) {
6467

6568
EntityManager em = getEntityManager();
6669
Class<?> type = getTypeToQueryFor(returnedType);
6770

68-
return type == null ? em.createNativeQuery(queryString) : em.createNativeQuery(queryString, type);
71+
return type == null ? em.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable))
72+
: em.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable), type);
6973
}
7074

7175
@Nullable

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@
1818
import static org.mockito.ArgumentMatchers.*;
1919
import static org.mockito.Mockito.*;
2020

21-
import java.lang.reflect.Method;
22-
import java.util.Set;
23-
2421
import jakarta.persistence.EntityManager;
2522
import jakarta.persistence.PersistenceContext;
2623
import jakarta.persistence.Tuple;
2724

25+
import java.lang.reflect.Method;
26+
import java.util.Set;
27+
2828
import org.junit.jupiter.api.Test;
2929
import org.junit.jupiter.api.extension.ExtendWith;
30-
30+
import org.springframework.data.domain.Sort;
3131
import org.springframework.data.jpa.domain.sample.Role;
3232
import org.springframework.data.jpa.domain.sample.User;
3333
import org.springframework.data.jpa.provider.PersistenceProvider;
@@ -60,10 +60,11 @@ void createsNormalQueryForJpaManagedReturnTypes() throws Exception {
6060
when(mock.getMetamodel()).thenReturn(em.getMetamodel());
6161

6262
JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class);
63-
AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock,
64-
null, QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser());
63+
AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, null,
64+
QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser());
6565

66-
jpaQuery.createJpaQuery(method.getAnnotatedQuery(), method.getResultProcessor().getReturnedType());
66+
jpaQuery.createJpaQuery(method.getAnnotatedQuery(), Sort.unsorted(), null,
67+
method.getResultProcessor().getReturnedType());
6768

6869
verify(mock, times(1)).createQuery(anyString());
6970
verify(mock, times(0)).createQuery(anyString(), eq(Tuple.class));

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,20 @@
1919
import static org.mockito.ArgumentMatchers.*;
2020
import static org.mockito.Mockito.*;
2121

22-
import java.lang.reflect.Method;
23-
import java.util.List;
24-
2522
import jakarta.persistence.EntityManager;
2623
import jakarta.persistence.EntityManagerFactory;
2724
import jakarta.persistence.metamodel.Metamodel;
2825

26+
import java.lang.reflect.Method;
27+
import java.util.List;
28+
2929
import org.junit.jupiter.api.BeforeEach;
3030
import org.junit.jupiter.api.Test;
3131
import org.junit.jupiter.api.extension.ExtendWith;
3232
import org.mockito.Mock;
3333
import org.mockito.junit.jupiter.MockitoExtension;
3434
import org.mockito.junit.jupiter.MockitoSettings;
3535
import org.mockito.quality.Strictness;
36-
3736
import org.springframework.data.domain.Page;
3837
import org.springframework.data.domain.Pageable;
3938
import org.springframework.data.domain.Sort;

0 commit comments

Comments
 (0)