diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/QueryRewriter.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/QueryRewriter.java
index d34101046..619d6231a 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/QueryRewriter.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/QueryRewriter.java
@@ -26,6 +26,10 @@ import org.springframework.data.domain.Sort;
* 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.
+ *
+ * Query rewriting applies to the actual query and, when applicable, to count queries. Count queries are optimized and
+ * therefore, either not necessary or a count is obtained through other means, such as derived from a Hibernate
+ * {@code SelectionQuery}.
*
* @author Greg Turnquist
* @author Mark Paluch
@@ -71,4 +75,5 @@ public interface QueryRewriter {
return query;
}
}
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
index 01f7af53f..91624e263 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
@@ -151,9 +151,11 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
String queryString = countQuery.get().getQueryString();
EntityManager em = getEntityManager();
+ String queryStringToUse = potentiallyRewriteQuery(queryString, accessor.getSort(), accessor.getPageable());
+
Query query = getQueryMethod().isNativeQuery() //
- ? em.createNativeQuery(queryString) //
- : em.createQuery(queryString, Long.class);
+ ? em.createNativeQuery(queryStringToUse) //
+ : em.createQuery(queryStringToUse, Long.class);
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryString, query);
@@ -184,16 +186,17 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
ReturnedType returnedType) {
EntityManager em = getEntityManager();
+ String queryToUse = potentiallyRewriteQuery(queryString, sort, pageable);
if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) {
- return em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable));
+ return em.createQuery(queryToUse);
}
Class> typeToRead = getTypeToRead(returnedType);
return typeToRead == null //
- ? em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable)) //
- : em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable), typeToRead);
+ ? em.createQuery(queryToUse) //
+ : em.createQuery(queryToUse, typeToRead);
}
/**
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
index d8daf8a6a..6d25af839 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
@@ -181,11 +181,9 @@ public final class JpaQueryLookupStrategy {
getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate);
}
- RepositoryQuery query = NamedQuery.lookupFrom(method, em);
+ RepositoryQuery query = NamedQuery.lookupFrom(method, em, queryRewriter);
- return query != null //
- ? query //
- : NO_QUERY;
+ return query != null ? query : NO_QUERY;
}
@Nullable
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
index 1e4c5fb8f..eeed1593f 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
@@ -22,7 +22,11 @@ import jakarta.persistence.TypedQuery;
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.provider.QueryExtractor;
+import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.RepositoryQuery;
@@ -53,11 +57,12 @@ final class NamedQuery extends AbstractJpaQuery {
private final boolean namedCountQueryIsPresent;
private final Lazy declaredQuery;
private final QueryParameterSetter.QueryMetadataCache metadataCache;
+ private final QueryRewriter queryRewriter;
/**
* Creates a new {@link NamedQuery}.
*/
- private NamedQuery(JpaQueryMethod method, EntityManager em) {
+ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryRewriter queryRewriter) {
super(method, em);
@@ -65,6 +70,7 @@ final class NamedQuery extends AbstractJpaQuery {
this.countQueryName = method.getNamedCountQueryName();
QueryExtractor extractor = method.getQueryExtractor();
this.countProjection = method.getCountQueryProjection();
+ this.queryRewriter = queryRewriter;
Parameters, ?> parameters = method.getParameters();
@@ -127,9 +133,10 @@ final class NamedQuery extends AbstractJpaQuery {
*
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
+ * @param queryRewriter must not be {@literal null}.
*/
@Nullable
- public static RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em) {
+ public static RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em, QueryRewriter queryRewriter) {
String queryName = method.getNamedQueryName();
@@ -147,7 +154,7 @@ final class NamedQuery extends AbstractJpaQuery {
method.isNativeQuery() ? "NativeQuery" : "Query"));
}
- RepositoryQuery query = new NamedQuery(method, em);
+ RepositoryQuery query = new NamedQuery(method, em, queryRewriter);
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Found named query '%s'", queryName));
}
@@ -187,6 +194,7 @@ final class NamedQuery extends AbstractJpaQuery {
} else {
String countQueryString = declaredQuery.get().deriveCountQuery(countProjection).getQueryString();
+ countQueryString = potentiallyRewriteQuery(countQueryString, accessor.getSort(), accessor.getPageable());
cacheKey = countQueryString;
countQuery = em.createQuery(countQueryString, Long.class);
}
@@ -222,4 +230,20 @@ final class NamedQuery extends AbstractJpaQuery {
? null //
: super.getTypeToRead(returnedType);
}
+
+ /**
+ * Use the {@link QueryRewriter}, potentially rewrite the query, using relevant {@link Sort} and {@link Pageable}
+ * information.
+ *
+ * @param originalQuery
+ * @param sort
+ * @param pageable
+ * @return
+ */
+ private String potentiallyRewriteQuery(String originalQuery, Sort sort, Pageable pageable) {
+
+ return pageable.isPaged() //
+ ? queryRewriter.rewrite(originalQuery, pageable) //
+ : queryRewriter.rewrite(originalQuery, sort);
+ }
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java
index 9738c7843..9637785e3 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java
@@ -19,8 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -31,6 +33,7 @@ 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.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -44,7 +47,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
- * Unit tests for repository with {@link Query} and {@link QueryRewrite}.
+ * Unit tests for repository with {@link Query} and {@link QueryRewriter}.
*
* @author Greg Turnquist
* @author Krzysztof Krason
@@ -60,10 +63,12 @@ class JpaQueryRewriteIntegrationTests {
static final String REWRITTEN_QUERY = "rewritten query";
static final String SORT = "sort";
static Map results = new HashMap<>();
+ static Set queries = new LinkedHashSet<>();
@BeforeEach
void setUp() {
results.clear();
+ repository.deleteAll();
}
@Test
@@ -77,15 +82,15 @@ class JpaQueryRewriteIntegrationTests {
entry(SORT, Sort.unsorted().toString()));
}
- @Test
+ @Test // GH-3801
void nonNativeQueryShouldHandleRewrites() {
- repository.findByNonNativeQuery("Matthews");
+ repository.save(new User("D", "A", "foo@bar"));
- 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()));
+ repository.findByNonNativeQuery("Matthews", PageRequest.of(0, 1));
+
+ assertThat(queries).contains("select original_user_alias from User original_user_alias");
+ assertThat(queries).contains("select count(original_user_alias) from User original_user_alias");
}
@Test
@@ -169,7 +174,7 @@ class JpaQueryRewriteIntegrationTests {
List findByNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class)
- List findByNonNativeQuery(String param);
+ Page findByNonNativeQuery(String param, PageRequest pageRequest);
@Query(value = "select original_user_alias from User original_user_alias", queryRewriter = TestQueryRewriter.class)
List findByNonNativeSortedQuery(String param, Sort sort);
@@ -214,6 +219,7 @@ class JpaQueryRewriteIntegrationTests {
results.put(ORIGINAL_QUERY, query);
results.put(REWRITTEN_QUERY, rewrittenQuery);
results.put(SORT, sort.toString());
+ queries.add(query);
return rewrittenQuery;
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java
index dadfd1083..68cae8bc6 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java
@@ -36,6 +36,7 @@ import org.mockito.quality.Strictness;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.provider.QueryExtractor;
+import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -88,7 +89,8 @@ class NamedQueryUnitTests {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, projectionFactory, extractor);
when(em.createNamedQuery(queryMethod.getNamedCountQueryName())).thenThrow(new IllegalArgumentException());
- assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em));
+ assertThatExceptionOfType(QueryCreationException.class)
+ .isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em, QueryRewriter.IdentityQueryRewriter.INSTANCE));
}
@Test // DATAJPA-142
@@ -100,7 +102,8 @@ class NamedQueryUnitTests {
TypedQuery countQuery = mock(TypedQuery.class);
when(em.createNamedQuery(eq(queryMethod.getNamedCountQueryName()), eq(Long.class))).thenReturn(countQuery);
- NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em);
+ NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em,
+ QueryRewriter.IdentityQueryRewriter.INSTANCE);
query.doCreateCountQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[1]));
verify(em, times(1)).createNamedQuery(queryMethod.getNamedCountQueryName(), Long.class);
diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc
index 7cd161017..e2f6112c8 100644
--- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc
+++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc
@@ -176,8 +176,11 @@ public interface UserRepository extends JpaRepository {
Sometimes, no matter how many features you try to apply, it seems impossible to get Spring Data JPA to apply every thing
you'd like to a query before it is sent to the `EntityManager`.
-You have the ability to get your hands on the query, right before it's sent to the `EntityManager` and "rewrite" it. That is,
-you can make any alterations at the last moment.
+You have the ability to get your hands on the query, right before it's sent to the `EntityManager` and "rewrite" it.
+That is, you can make any alterations at the last moment.
+Query rewriting applies to the actual query and, when applicable, to count queries.
+Count queries are optimized and therefore, either not necessary or a count is obtained through other means, such as derived from a Hibernate `SelectionQuery`.
+
.Declare a QueryRewriter using `@Query`
====