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:
Mark Paluch
2021-10-13 12:05:49 +02:00
parent 87da84b7e7
commit 711482e715
9 changed files with 120 additions and 48 deletions

View File

@@ -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;

View File

@@ -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);
}
/**

View File

@@ -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;
}
}
/**

View File

@@ -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

View File

@@ -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();

View File

@@ -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);

View File

@@ -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());

View File

@@ -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();
}

View File

@@ -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 {