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
This commit is contained in:
@@ -22,6 +22,7 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro
|
||||
import org.springframework.data.repository.query.ResultProcessor;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -49,10 +50,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
* @param method must not be {@literal null}.
|
||||
* @param em must not be {@literal null}.
|
||||
* @param queryString must not be {@literal null}.
|
||||
* @param countQueryString must not be {@literal null}.
|
||||
* @param evaluationContextProvider must not be {@literal null}.
|
||||
* @param parser must not be {@literal null}.
|
||||
*/
|
||||
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
|
||||
@Nullable String countQueryString,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
|
||||
|
||||
super(method, em);
|
||||
@@ -64,7 +67,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser);
|
||||
|
||||
DeclaredQuery countQuery = query.deriveCountQuery(method.getCountQuery(), method.getCountQueryProjection());
|
||||
DeclaredQuery countQuery = query.deriveCountQuery(countQueryString, method.getCountQueryProjection());
|
||||
this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser);
|
||||
|
||||
this.parser = parser;
|
||||
|
||||
@@ -19,8 +19,7 @@ import javax.persistence.EntityManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
@@ -39,42 +38,28 @@ enum JpaQueryFactory {
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JpaQueryFactory.class);
|
||||
|
||||
/**
|
||||
* Creates a {@link RepositoryQuery} from the given {@link QueryMethod} that is potentially annotated with
|
||||
* {@link Query}.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param em must not be {@literal null}.
|
||||
* @param evaluationContextProvider
|
||||
* @return the {@link RepositoryQuery} derived from the annotation or {@code null} if no annotation found.
|
||||
*/
|
||||
@Nullable
|
||||
AbstractJpaQuery fromQueryAnnotation(JpaQueryMethod method, EntityManager em,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
|
||||
LOG.debug("Looking up query for method {}", method.getName());
|
||||
return fromMethodWithQueryString(method, em, method.getAnnotatedQuery(), evaluationContextProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link RepositoryQuery} from the given {@link String} query.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param em must not be {@literal null}.
|
||||
* @param queryString must not be {@literal null} or empty.
|
||||
* @param countQueryString
|
||||
* @param evaluationContextProvider
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, @Nullable String queryString,
|
||||
@Nullable String countQueryString,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
|
||||
if (queryString == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return method.isNativeQuery() ? new NativeJpaQuery(method, em, queryString, evaluationContextProvider, PARSER)
|
||||
: new SimpleJpaQuery(method, em, queryString, evaluationContextProvider, PARSER);
|
||||
return method.isNativeQuery()
|
||||
? new NativeJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER)
|
||||
: new SimpleJpaQuery(method, em, queryString, countQueryString, evaluationContextProvider, PARSER);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Query lookup strategy to execute finders.
|
||||
@@ -148,18 +149,20 @@ public final class JpaQueryLookupStrategy {
|
||||
@Override
|
||||
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
|
||||
|
||||
RepositoryQuery query = JpaQueryFactory.INSTANCE.fromQueryAnnotation(method, em, evaluationContextProvider);
|
||||
String countQuery = getCountQuery(method, namedQueries, em);
|
||||
|
||||
if (query != null && method.hasAnnotatedQueryName()) {
|
||||
LOG.warn(String.format(
|
||||
"Query method %s is annotated with both, a query and a query name. Using the declared query.", method));
|
||||
if (StringUtils.hasText(method.getAnnotatedQuery())) {
|
||||
|
||||
if (method.hasAnnotatedQueryName()) {
|
||||
LOG.warn(String.format(
|
||||
"Query method %s is annotated with both, a query and a query name. Using the declared query.", method));
|
||||
}
|
||||
|
||||
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getAnnotatedQuery(), countQuery,
|
||||
evaluationContextProvider);
|
||||
}
|
||||
|
||||
if (null != query) {
|
||||
return query;
|
||||
}
|
||||
|
||||
query = JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em);
|
||||
RepositoryQuery query = JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em);
|
||||
|
||||
if (null != query) {
|
||||
return query;
|
||||
@@ -167,7 +170,7 @@ public final class JpaQueryLookupStrategy {
|
||||
|
||||
String name = method.getNamedQueryName();
|
||||
if (namedQueries.hasQuery(name)) {
|
||||
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name),
|
||||
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), countQuery,
|
||||
evaluationContextProvider);
|
||||
}
|
||||
|
||||
@@ -180,6 +183,32 @@ public final class JpaQueryLookupStrategy {
|
||||
throw new IllegalStateException(
|
||||
String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) {
|
||||
|
||||
if (StringUtils.hasText(method.getCountQuery())) {
|
||||
return method.getCountQuery();
|
||||
}
|
||||
|
||||
String queryName = method.getNamedCountQueryName();
|
||||
|
||||
if (!StringUtils.hasText(queryName)) {
|
||||
return method.getCountQuery();
|
||||
}
|
||||
|
||||
if (namedQueries.hasQuery(queryName)) {
|
||||
return namedQueries.getQuery(queryName);
|
||||
}
|
||||
|
||||
boolean namedQuery = NamedQuery.hasNamedQuery(em, queryName);
|
||||
|
||||
if (namedQuery) {
|
||||
return method.getQueryExtractor().extractQueryString(em.createNamedQuery(queryName));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.*;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.Query;
|
||||
import javax.persistence.Tuple;
|
||||
@@ -105,7 +103,7 @@ final class NamedQuery extends AbstractJpaQuery {
|
||||
* @param queryName must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean hasNamedQuery(EntityManager em, String queryName) {
|
||||
static boolean hasNamedQuery(EntityManager em, String queryName) {
|
||||
|
||||
/*
|
||||
* See DATAJPA-617, we have to use a dedicated em for the lookups to avoid a
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Thomas Darimont
|
||||
* @author Oliver Gierke
|
||||
* @author Jens Schauder
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
|
||||
|
||||
@@ -43,12 +44,13 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
|
||||
* @param method must not be {@literal null}.
|
||||
* @param em must not be {@literal null}.
|
||||
* @param queryString must not be {@literal null} or empty.
|
||||
* @param countQueryString must not be {@literal null} or empty.
|
||||
* @param evaluationContextProvider
|
||||
*/
|
||||
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
|
||||
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
|
||||
|
||||
super(method, em, queryString, evaluationContextProvider, parser);
|
||||
super(method, em, queryString, countQueryString, evaluationContextProvider, parser);
|
||||
|
||||
Parameters<?, ?> parameters = method.getParameters();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import javax.persistence.Query;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link RepositoryQuery} implementation that inspects a {@link org.springframework.data.repository.query.QueryMethod}
|
||||
@@ -38,12 +39,13 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
|
||||
*
|
||||
* @param method must not be {@literal null}
|
||||
* @param em must not be {@literal null}
|
||||
* @param countQueryString
|
||||
* @param evaluationContextProvider must not be {@literal null}
|
||||
* @param parser must not be {@literal null}
|
||||
*/
|
||||
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em,
|
||||
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
|
||||
this(method, em, method.getRequiredAnnotatedQuery(), evaluationContextProvider, parser);
|
||||
this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, evaluationContextProvider, parser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,13 +54,14 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
|
||||
* @param method must not be {@literal null}
|
||||
* @param em must not be {@literal null}
|
||||
* @param queryString must not be {@literal null} or empty
|
||||
* @param countQueryString
|
||||
* @param evaluationContextProvider must not be {@literal null}
|
||||
* @param parser must not be {@literal null}
|
||||
*/
|
||||
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
|
||||
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
|
||||
|
||||
super(method, em, queryString, evaluationContextProvider, parser);
|
||||
super(method, em, queryString, countQueryString, evaluationContextProvider, parser);
|
||||
|
||||
validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s!", method);
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ public class AbstractStringBasedJpaQueryIntegrationTests {
|
||||
|
||||
JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class);
|
||||
AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock,
|
||||
QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser());
|
||||
null, QueryMethodEvaluationContextProvider.DEFAULT, new SpelExpressionParser());
|
||||
|
||||
jpaQuery.createJpaQuery(method.getAnnotatedQuery(), method.getResultProcessor().getReturnedType());
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.provider.QueryExtractor;
|
||||
@@ -111,6 +113,47 @@ public class JpaQueryLookupStrategyUnitTests {
|
||||
.withMessageContaining(method.toString());
|
||||
}
|
||||
|
||||
@Test // GH-2217
|
||||
void considersNamedCountQuery() throws Exception {
|
||||
|
||||
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
|
||||
EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT);
|
||||
|
||||
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
|
||||
when(namedQueries.getQuery("foo.count")).thenReturn("foo count");
|
||||
|
||||
when(namedQueries.hasQuery("User.findByNamedQuery")).thenReturn(true);
|
||||
when(namedQueries.getQuery("User.findByNamedQuery")).thenReturn("select foo");
|
||||
|
||||
Method method = UserRepository.class.getMethod("findByNamedQuery", String.class, Pageable.class);
|
||||
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
|
||||
|
||||
RepositoryQuery repositoryQuery = strategy.resolveQuery(method, metadata, projectionFactory, namedQueries);
|
||||
assertThat(repositoryQuery).isInstanceOf(SimpleJpaQuery.class);
|
||||
SimpleJpaQuery query = (SimpleJpaQuery) repositoryQuery;
|
||||
assertThat(query.getQuery().getQueryString()).isEqualTo("select foo");
|
||||
assertThat(query.getCountQuery().getQueryString()).isEqualTo("foo count");
|
||||
}
|
||||
|
||||
@Test // GH-2217
|
||||
void considersNamedCountOnStringQueryQuery() throws Exception {
|
||||
|
||||
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
|
||||
EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT);
|
||||
|
||||
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
|
||||
when(namedQueries.getQuery("foo.count")).thenReturn("foo count");
|
||||
|
||||
Method method = UserRepository.class.getMethod("findByStringQueryWithNamedCountQuery", String.class,
|
||||
Pageable.class);
|
||||
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
|
||||
|
||||
RepositoryQuery repositoryQuery = strategy.resolveQuery(method, metadata, projectionFactory, namedQueries);
|
||||
assertThat(repositoryQuery).isInstanceOf(SimpleJpaQuery.class);
|
||||
SimpleJpaQuery query = (SimpleJpaQuery) repositoryQuery;
|
||||
assertThat(query.getCountQuery().getQueryString()).isEqualTo("foo count");
|
||||
}
|
||||
|
||||
@Test // GH-2319
|
||||
void prefersDeclaredQuery() throws Exception {
|
||||
|
||||
@@ -132,6 +175,12 @@ public class JpaQueryLookupStrategyUnitTests {
|
||||
@Query(value = "select u.* from User u", nativeQuery = true)
|
||||
List<User> findByInvalidNativeQuery(String param, Sort sort);
|
||||
|
||||
@Query(countName = "foo.count")
|
||||
Page<User> findByNamedQuery(String foo, Pageable pageable);
|
||||
|
||||
@Query(value = "foo.query", countName = "foo.count")
|
||||
Page<User> findByStringQueryWithNamedCountQuery(String foo, Pageable pageable);
|
||||
|
||||
@Query(value = "something absurd", name = "my-query-name")
|
||||
User annotatedQueryWithQueryAndQueryName();
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class SimpleJpaQueryUnitTests {
|
||||
metadata, factory, extractor);
|
||||
when(em.createQuery("foo", Long.class)).thenReturn(typedQuery);
|
||||
|
||||
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", EVALUATION_CONTEXT_PROVIDER,
|
||||
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, EVALUATION_CONTEXT_PROVIDER,
|
||||
PARSER);
|
||||
|
||||
assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {})))
|
||||
@@ -126,7 +126,8 @@ class SimpleJpaQueryUnitTests {
|
||||
Method method = UserRepository.class.getMethod("findAllPaged", Pageable.class);
|
||||
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
|
||||
|
||||
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", EVALUATION_CONTEXT_PROVIDER,
|
||||
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null,
|
||||
EVALUATION_CONTEXT_PROVIDER,
|
||||
PARSER);
|
||||
jpaQuery.createCountQuery(
|
||||
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
|
||||
@@ -141,8 +142,8 @@ class SimpleJpaQueryUnitTests {
|
||||
|
||||
Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class);
|
||||
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
|
||||
AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em,
|
||||
EVALUATION_CONTEXT_PROVIDER);
|
||||
AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em,
|
||||
queryMethod.getAnnotatedQuery(), null, EVALUATION_CONTEXT_PROVIDER);
|
||||
|
||||
assertThat(jpaQuery instanceof NativeJpaQuery).isTrue();
|
||||
|
||||
@@ -244,7 +245,8 @@ class SimpleJpaQueryUnitTests {
|
||||
Method method = SampleRepository.class.getMethod("findAllWithExpressionInCountQuery", Pageable.class);
|
||||
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
|
||||
|
||||
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", EVALUATION_CONTEXT_PROVIDER,
|
||||
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u",
|
||||
"select count(u.id) from #{#entityName} u", EVALUATION_CONTEXT_PROVIDER,
|
||||
PARSER);
|
||||
jpaQuery.createCountQuery(
|
||||
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
|
||||
@@ -256,7 +258,8 @@ class SimpleJpaQueryUnitTests {
|
||||
private AbstractJpaQuery createJpaQuery(Method method) {
|
||||
|
||||
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
|
||||
return JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em, EVALUATION_CONTEXT_PROVIDER);
|
||||
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery(), null,
|
||||
EVALUATION_CONTEXT_PROVIDER);
|
||||
}
|
||||
|
||||
interface SampleRepository {
|
||||
|
||||
Reference in New Issue
Block a user