Skip to content

Commit 711482e

Browse files
committed
Consider named count query for named queries.
We now consider `@Query(countName = "…")` when the actual query is a string query or named query using Properties to declare its query. Named queries using JPA named queries remain unchanged. Closes #2217
1 parent 87da84b commit 711482e

File tree

9 files changed

+119
-47
lines changed

9 files changed

+119
-47
lines changed

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.data.repository.query.ResultProcessor;
2323
import org.springframework.data.repository.query.ReturnedType;
2424
import org.springframework.expression.spel.standard.SpelExpressionParser;
25+
import org.springframework.lang.Nullable;
2526
import org.springframework.util.Assert;
2627

2728
/**
@@ -49,10 +50,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
4950
* @param method must not be {@literal null}.
5051
* @param em must not be {@literal null}.
5152
* @param queryString must not be {@literal null}.
53+
* @param countQueryString must not be {@literal null}.
5254
* @param evaluationContextProvider must not be {@literal null}.
5355
* @param parser must not be {@literal null}.
5456
*/
5557
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
58+
@Nullable String countQueryString,
5659
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
5760

5861
super(method, em);
@@ -64,7 +67,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
6467
this.evaluationContextProvider = evaluationContextProvider;
6568
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser);
6669

67-
DeclaredQuery countQuery = query.deriveCountQuery(method.getCountQuery(), method.getCountQueryProjection());
70+
DeclaredQuery countQuery = query.deriveCountQuery(countQueryString, method.getCountQueryProjection());
6871
this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser);
6972

7073
this.parser = parser;

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

+6-21
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919

2020
import org.slf4j.Logger;
2121
import org.slf4j.LoggerFactory;
22-
import org.springframework.data.jpa.repository.Query;
23-
import org.springframework.data.repository.query.QueryMethod;
22+
2423
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2524
import org.springframework.data.repository.query.RepositoryQuery;
2625
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -39,42 +38,28 @@ enum JpaQueryFactory {
3938
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
4039
private static final Logger LOG = LoggerFactory.getLogger(JpaQueryFactory.class);
4140

42-
/**
43-
* Creates a {@link RepositoryQuery} from the given {@link QueryMethod} that is potentially annotated with
44-
* {@link Query}.
45-
*
46-
* @param method must not be {@literal null}.
47-
* @param em must not be {@literal null}.
48-
* @param evaluationContextProvider
49-
* @return the {@link RepositoryQuery} derived from the annotation or {@code null} if no annotation found.
50-
*/
51-
@Nullable
52-
AbstractJpaQuery fromQueryAnnotation(JpaQueryMethod method, EntityManager em,
53-
QueryMethodEvaluationContextProvider evaluationContextProvider) {
54-
55-
LOG.debug("Looking up query for method {}", method.getName());
56-
return fromMethodWithQueryString(method, em, method.getAnnotatedQuery(), evaluationContextProvider);
57-
}
58-
5941
/**
6042
* Creates a {@link RepositoryQuery} from the given {@link String} query.
6143
*
6244
* @param method must not be {@literal null}.
6345
* @param em must not be {@literal null}.
6446
* @param queryString must not be {@literal null} or empty.
47+
* @param countQueryString
6548
* @param evaluationContextProvider
6649
* @return
6750
*/
6851
@Nullable
6952
AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, @Nullable String queryString,
53+
@Nullable String countQueryString,
7054
QueryMethodEvaluationContextProvider evaluationContextProvider) {
7155

7256
if (queryString == null) {
7357
return null;
7458
}
7559

76-
return method.isNativeQuery() ? new NativeJpaQuery(method, em, queryString, evaluationContextProvider, PARSER)
77-
: new SimpleJpaQuery(method, em, queryString, evaluationContextProvider, PARSER);
60+
return method.isNativeQuery()
61+
? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER)
62+
: new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER);
7863
}
7964

8065
/**

src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java

+38-9
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.repository.query.RepositoryQuery;
3333
import org.springframework.lang.Nullable;
3434
import org.springframework.util.Assert;
35+
import org.springframework.util.StringUtils;
3536

3637
/**
3738
* Query lookup strategy to execute finders.
@@ -148,26 +149,28 @@ public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory query
148149
@Override
149150
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
150151

151-
RepositoryQuery query = JpaQueryFactory.INSTANCE.fromQueryAnnotation(method, em, evaluationContextProvider);
152+
String countQuery = getCountQuery(method, namedQueries, em);
152153

153-
if (query != null && method.hasAnnotatedQueryName()) {
154-
LOG.warn(String.format(
155-
"Query method %s is annotated with both, a query and a query name. Using the declared query.", method));
156-
}
154+
if (StringUtils.hasText(method.getAnnotatedQuery())) {
157155

158-
if (null != query) {
159-
return query;
156+
if (method.hasAnnotatedQueryName()) {
157+
LOG.warn(String.format(
158+
"Query method %s is annotated with both, a query and a query name. Using the declared query.", method));
159+
}
160+
161+
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getAnnotatedQuery(), countQuery,
162+
evaluationContextProvider);
160163
}
161164

162-
query = JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em);
165+
RepositoryQuery query = JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em);
163166

164167
if (null != query) {
165168
return query;
166169
}
167170

168171
String name = method.getNamedQueryName();
169172
if (namedQueries.hasQuery(name)) {
170-
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name),
173+
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), countQuery,
171174
evaluationContextProvider);
172175
}
173176

@@ -180,6 +183,32 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em,
180183
throw new IllegalStateException(
181184
String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method));
182185
}
186+
187+
@Nullable
188+
private String getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) {
189+
190+
if (StringUtils.hasText(method.getCountQuery())) {
191+
return method.getCountQuery();
192+
}
193+
194+
String queryName = method.getNamedCountQueryName();
195+
196+
if (!StringUtils.hasText(queryName)) {
197+
return method.getCountQuery();
198+
}
199+
200+
if (namedQueries.hasQuery(queryName)) {
201+
return namedQueries.getQuery(queryName);
202+
}
203+
204+
boolean namedQuery = NamedQuery.hasNamedQuery(em, queryName);
205+
206+
if (namedQuery) {
207+
return method.getQueryExtractor().extractQueryString(em.createNamedQuery(queryName));
208+
}
209+
210+
return null;
211+
}
183212
}
184213

185214
/**

src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java

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

18-
import static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.*;
19-
2018
import javax.persistence.EntityManager;
2119
import javax.persistence.Query;
2220
import javax.persistence.Tuple;
@@ -105,7 +103,7 @@ private NamedQuery(JpaQueryMethod method, EntityManager em) {
105103
* @param queryName must not be {@literal null}.
106104
* @return
107105
*/
108-
private static boolean hasNamedQuery(EntityManager em, String queryName) {
106+
static boolean hasNamedQuery(EntityManager em, String queryName) {
109107

110108
/*
111109
* See DATAJPA-617, we have to use a dedicated em for the lookups to avoid a

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
* @author Thomas Darimont
3535
* @author Oliver Gierke
3636
* @author Jens Schauder
37+
* @author Mark Paluch
3738
*/
3839
final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
3940

@@ -43,12 +44,13 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
4344
* @param method must not be {@literal null}.
4445
* @param em must not be {@literal null}.
4546
* @param queryString must not be {@literal null} or empty.
47+
* @param countQueryString must not be {@literal null} or empty.
4648
* @param evaluationContextProvider
4749
*/
48-
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
50+
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
4951
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
5052

51-
super(method, em, queryString, evaluationContextProvider, parser);
53+
super(method, em, queryString, countQueryString, evaluationContextProvider, parser);
5254

5355
Parameters<?, ?> parameters = method.getParameters();
5456

src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2222
import org.springframework.data.repository.query.RepositoryQuery;
2323
import org.springframework.expression.spel.standard.SpelExpressionParser;
24+
import org.springframework.lang.Nullable;
2425

2526
/**
2627
* {@link RepositoryQuery} implementation that inspects a {@link org.springframework.data.repository.query.QueryMethod}
@@ -38,12 +39,13 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
3839
*
3940
* @param method must not be {@literal null}
4041
* @param em must not be {@literal null}
42+
* @param countQueryString
4143
* @param evaluationContextProvider must not be {@literal null}
4244
* @param parser must not be {@literal null}
4345
*/
44-
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em,
46+
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString,
4547
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
46-
this(method, em, method.getRequiredAnnotatedQuery(), evaluationContextProvider, parser);
48+
this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, evaluationContextProvider, parser);
4749
}
4850

4951
/**
@@ -52,13 +54,14 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em,
5254
* @param method must not be {@literal null}
5355
* @param em must not be {@literal null}
5456
* @param queryString must not be {@literal null} or empty
57+
* @param countQueryString
5558
* @param evaluationContextProvider must not be {@literal null}
5659
* @param parser must not be {@literal null}
5760
*/
58-
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
61+
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
5962
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
6063

61-
super(method, em, queryString, evaluationContextProvider, parser);
64+
super(method, em, queryString, countQueryString, evaluationContextProvider, parser);
6265

6366
validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s!", method);
6467

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ void createsNormalQueryForJpaManagedReturnTypes() throws Exception {
6161

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

6666
jpaQuery.createJpaQuery(method.getAnnotatedQuery(), method.getResultProcessor().getReturnedType());
6767

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

+49
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.mockito.junit.jupiter.MockitoSettings;
3535
import org.mockito.quality.Strictness;
3636

37+
import org.springframework.data.domain.Page;
38+
import org.springframework.data.domain.Pageable;
3739
import org.springframework.data.domain.Sort;
3840
import org.springframework.data.jpa.domain.sample.User;
3941
import org.springframework.data.jpa.provider.QueryExtractor;
@@ -111,6 +113,47 @@ void sholdThrowMorePreciseExceptionIfTryingToUsePaginationInNativeQueries() thro
111113
.withMessageContaining(method.toString());
112114
}
113115

116+
@Test // GH-2217
117+
void considersNamedCountQuery() throws Exception {
118+
119+
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
120+
EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT);
121+
122+
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
123+
when(namedQueries.getQuery("foo.count")).thenReturn("foo count");
124+
125+
when(namedQueries.hasQuery("User.findByNamedQuery")).thenReturn(true);
126+
when(namedQueries.getQuery("User.findByNamedQuery")).thenReturn("select foo");
127+
128+
Method method = UserRepository.class.getMethod("findByNamedQuery", String.class, Pageable.class);
129+
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
130+
131+
RepositoryQuery repositoryQuery = strategy.resolveQuery(method, metadata, projectionFactory, namedQueries);
132+
assertThat(repositoryQuery).isInstanceOf(SimpleJpaQuery.class);
133+
SimpleJpaQuery query = (SimpleJpaQuery) repositoryQuery;
134+
assertThat(query.getQuery().getQueryString()).isEqualTo("select foo");
135+
assertThat(query.getCountQuery().getQueryString()).isEqualTo("foo count");
136+
}
137+
138+
@Test // GH-2217
139+
void considersNamedCountOnStringQueryQuery() throws Exception {
140+
141+
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
142+
EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT);
143+
144+
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
145+
when(namedQueries.getQuery("foo.count")).thenReturn("foo count");
146+
147+
Method method = UserRepository.class.getMethod("findByStringQueryWithNamedCountQuery", String.class,
148+
Pageable.class);
149+
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
150+
151+
RepositoryQuery repositoryQuery = strategy.resolveQuery(method, metadata, projectionFactory, namedQueries);
152+
assertThat(repositoryQuery).isInstanceOf(SimpleJpaQuery.class);
153+
SimpleJpaQuery query = (SimpleJpaQuery) repositoryQuery;
154+
assertThat(query.getCountQuery().getQueryString()).isEqualTo("foo count");
155+
}
156+
114157
@Test // GH-2319
115158
void prefersDeclaredQuery() throws Exception {
116159

@@ -132,6 +175,12 @@ interface UserRepository extends Repository<User, Integer> {
132175
@Query(value = "select u.* from User u", nativeQuery = true)
133176
List<User> findByInvalidNativeQuery(String param, Sort sort);
134177

178+
@Query(countName = "foo.count")
179+
Page<User> findByNamedQuery(String foo, Pageable pageable);
180+
181+
@Query(value = "foo.query", countName = "foo.count")
182+
Page<User> findByStringQueryWithNamedCountQuery(String foo, Pageable pageable);
183+
135184
@Query(value = "something absurd", name = "my-query-name")
136185
User annotatedQueryWithQueryAndQueryName();
137186
}

src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java

+9-6
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ void prefersDeclaredCountQueryOverCreatingOne() throws Exception {
111111
metadata, factory, extractor);
112112
when(em.createQuery("foo", Long.class)).thenReturn(typedQuery);
113113

114-
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", EVALUATION_CONTEXT_PROVIDER,
114+
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, EVALUATION_CONTEXT_PROVIDER,
115115
PARSER);
116116

117117
assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {})))
@@ -126,7 +126,8 @@ void doesNotApplyPaginationToCountQuery() throws Exception {
126126
Method method = UserRepository.class.getMethod("findAllPaged", Pageable.class);
127127
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
128128

129-
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", EVALUATION_CONTEXT_PROVIDER,
129+
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null,
130+
EVALUATION_CONTEXT_PROVIDER,
130131
PARSER);
131132
jpaQuery.createCountQuery(
132133
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
@@ -141,8 +142,8 @@ void discoversNativeQuery() throws Exception {
141142

142143
Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class);
143144
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
144-
AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em,
145-
EVALUATION_CONTEXT_PROVIDER);
145+
AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em,
146+
queryMethod.getAnnotatedQuery(), null, EVALUATION_CONTEXT_PROVIDER);
146147

147148
assertThat(jpaQuery instanceof NativeJpaQuery).isTrue();
148149

@@ -244,7 +245,8 @@ void resolvesExpressionInCountQuery() throws Exception {
244245
Method method = SampleRepository.class.getMethod("findAllWithExpressionInCountQuery", Pageable.class);
245246
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
246247

247-
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", EVALUATION_CONTEXT_PROVIDER,
248+
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u",
249+
"select count(u.id) from #{#entityName} u", EVALUATION_CONTEXT_PROVIDER,
248250
PARSER);
249251
jpaQuery.createCountQuery(
250252
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
@@ -256,7 +258,8 @@ void resolvesExpressionInCountQuery() throws Exception {
256258
private AbstractJpaQuery createJpaQuery(Method method) {
257259

258260
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
259-
return JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em, EVALUATION_CONTEXT_PROVIDER);
261+
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery(), null,
262+
EVALUATION_CONTEXT_PROVIDER);
260263
}
261264

262265
interface SampleRepository {

0 commit comments

Comments
 (0)