diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 9496900a3..d8b19a925 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -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; diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java index 79a4366f8..16e60825f 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java @@ -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); } /** diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index 89d592420..86288ad9c 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -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; + } } /** diff --git a/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index 5ee7c1448..b6d473a11 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -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 diff --git a/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index da0d71897..b31753f2a 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -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(); diff --git a/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index 93a9bf405..9fcff1866 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -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); diff --git a/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java index 8e4864a6c..55feb1470 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java @@ -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()); diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index c5b8f7bac..79699c420 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -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 findByInvalidNativeQuery(String param, Sort sort); + @Query(countName = "foo.count") + Page findByNamedQuery(String foo, Pageable pageable); + + @Query(value = "foo.query", countName = "foo.count") + Page findByStringQueryWithNamedCountQuery(String foo, Pageable pageable); + @Query(value = "something absurd", name = "my-query-name") User annotatedQueryWithQueryAndQueryName(); } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index e8212840d..71339337e 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -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 {