From 1a14e92e69ade7dd9cb8928e2bdf0f14a8b4874b Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 11 Jan 2012 15:12:05 +0100 Subject: [PATCH] DATAJPA-142 - Allow using a NamedQuery for count queries in pagination. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We will now prefer a declared NamedQuery for count queries instead of deriving it from the actual NamedQuery under the following conditions: - by default a query named ${namedQueryName}.count exists - the name of the NamedQuery to be used is defined in @Query(countQueryName = "…") Note that a potential reconfiguration of the NamedQuery name will be taken into account for the according count query name to assure symmetry. Examples Page findByLastname(String lastname, Pageable pageable) NamedQuery name: User.findByLastname Count NamedQuery name: User.findByLastname.count @Query(name = "Foo.bar") Page findByLastname(String lastname, Pageable pageable) NamedQuery name: Foo.bar Count NamedQuery name: Foo.bar.count @Query(countName = "Foo.bar.count") Page findByLastname(String lastname, Pageable pageable) NamedQuery name: User.findByLastname Count NamedQuery name: Foo.bar.count @Query(name = "Foo.bar", countName = "something") Page findByLastname(String lastname, Pageable pageable) NamedQuery name: Foo.bar Count NamedQuery name: something --- .../data/jpa/repository/Query.java | 13 +++++- .../jpa/repository/query/JpaQueryMethod.java | 11 +++++ .../data/jpa/repository/query/NamedQuery.java | 42 ++++++++++++++----- .../query/JpaQueryMethodUnitTests.java | 24 +++++++++++ .../repository/query/NamedQueryUnitTests.java | 21 +++++++++- 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/springframework/data/jpa/repository/Query.java b/src/main/java/org/springframework/data/jpa/repository/Query.java index ccce385b1..736ec5e9b 100644 --- a/src/main/java/org/springframework/data/jpa/repository/Query.java +++ b/src/main/java/org/springframework/data/jpa/repository/Query.java @@ -21,6 +21,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import javax.persistence.NamedQuery; + /** * Annotation to declare finder queries directly on repository methods. * @@ -48,8 +50,17 @@ public @interface Query { boolean nativeQuery() default false; /** - * The named query to be used. If not defined, a NamedQuery with name of {@code $ domainClass}.${finderMethodName}} + * The named query to be used. If not defined, a NamedQuery with name of {@code $ domainClass}.${queryMethodName}} * will be used. */ String name() default ""; + + /** + * Returns the name of the {@link NamedQuery} to be used to execute count queries when pagination is used. Will + * default to the named query name configured suffixed by {@code .count}. + * + * @see #name() + * @return + */ + String countName() default ""; } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index 73cf1454a..a134701da 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java @@ -182,6 +182,17 @@ public class JpaQueryMethod extends QueryMethod { return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName(); } + /** + * Returns the name of the {@link NamedQuery} that shall be used for count queries. + * + * @return + */ + String getNamedCountQueryName() { + + String annotatedName = getAnnotationValue("countName", String.class); + return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName() + ".count"; + } + /** * Returns whether we should clear automatically for modifying queries. * 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 5a8269efa..9e9a6e6c2 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 @@ -41,6 +41,7 @@ final class NamedQuery extends AbstractJpaQuery { private static final Logger LOG = LoggerFactory.getLogger(NamedQuery.class); private final String queryName; + private final String countQueryName; private final QueryExtractor extractor; /** @@ -51,6 +52,7 @@ final class NamedQuery extends AbstractJpaQuery { super(method, em); this.queryName = method.getNamedQueryName(); + this.countQueryName = method.getNamedCountQueryName(); this.extractor = method.getQueryExtractor(); Parameters parameters = method.getParameters(); @@ -65,19 +67,32 @@ final class NamedQuery extends AbstractJpaQuery { + "via this Pageable will not be applied!", method); } - boolean weNeedToCreateCountQuery = method.getParameters().hasPageableParameter(); + boolean weNeedToCreateCountQuery = !hasNamedQuery(em, countQueryName) + && method.getParameters().hasPageableParameter(); boolean cantExtractQuery = !this.extractor.canExtractQuery(); if (weNeedToCreateCountQuery && cantExtractQuery) { throw QueryCreationException.create(method, CANNOT_EXTRACT_QUERY); } - Query query = em.createNamedQuery(queryName); + // Let's see if the referenced named query exists + em.createNamedQuery(queryName); + } - // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=322579 - // until it gets fixed - if (null != query) { - query.getHints(); + /** + * Returns whether the named query with the given name exists. + * + * @param em + * @return + */ + private static final boolean hasNamedQuery(EntityManager em, String queryName) { + + try { + em.createNamedQuery(queryName); + return true; + } catch (IllegalArgumentException e) { + LOG.debug("Did not find named query {}", queryName); + return false; } } @@ -118,10 +133,17 @@ final class NamedQuery extends AbstractJpaQuery { @Override protected TypedQuery doCreateCountQuery(Object[] values) { - Query query = createQuery(values); - String queryString = extractor.extractQueryString(query); + EntityManager em = getEntityManager(); + TypedQuery countQuery = null; - return createBinder(values).bind( - getEntityManager().createQuery(QueryUtils.createCountQueryFor(queryString), Long.class)); + if (hasNamedQuery(em, countQueryName)) { + countQuery = em.createNamedQuery(countQueryName, Long.class); + } else { + Query query = createQuery(values); + String queryString = extractor.extractQueryString(query); + countQuery = em.createQuery(QueryUtils.createCountQueryFor(queryString), Long.class); + } + + return createBinder(values).bind(countQuery); } } \ No newline at end of file diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java index e40bfc899..927e5793c 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.query; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.List; @@ -230,6 +231,29 @@ public class JpaQueryMethodUnitTests { assertEquals(LockModeType.PESSIMISTIC_WRITE, lockMode); } + /** + * @see DATAJPA-142 + */ + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void returnsDefaultCountQueryName() { + + when(metadata.getReturnedDomainClass(repositoryMethod)).thenReturn((Class) User.class); + + JpaQueryMethod method = new JpaQueryMethod(repositoryMethod, metadata, extractor); + assertThat(method.getNamedCountQueryName(), is("User.findByLastname.count")); + } + + /** + * @see DATAJPA-142 + */ + @Test + public void returnsDefaultCountQueryNameBasedOnConfiguredNamedQueryName() { + + JpaQueryMethod method = new JpaQueryMethod(namedQuery, metadata, extractor); + assertThat(method.getNamedCountQueryName(), is("Foo.bar.count")); + } + /** * Interface to define invalid repository methods for testing. * diff --git a/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java index e0aef304c..9096adb65 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jpa.repository.query; +import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -61,11 +62,29 @@ public class NamedQueryUnitTests { public void rejectsPersistenceProviderIfIncapableOfExtractingQueriesAndPagebleBeingUsed() { when(extractor.canExtractQuery()).thenReturn(false); - JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, extractor); + + when(em.createNamedQuery(queryMethod.getNamedCountQueryName())).thenThrow(new IllegalArgumentException()); NamedQuery.lookupFrom(queryMethod, em); } + /** + * @see DATAJPA-142 + */ + @Test + public void doesNotRejectPersistenceProviderIfNamedCountQueryIsAvailable() { + + when(extractor.canExtractQuery()).thenReturn(false); + JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, extractor); + + when(em.createNamedQuery(queryMethod.getNamedCountQueryName())).thenReturn(null); + NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em); + + query.doCreateCountQuery(new Object[1]); + verify(em, times(1)).createNamedQuery(queryMethod.getNamedCountQueryName(), Long.class); + verify(em, never()).createQuery(any(String.class), eq(Long.class)); + } + interface SampleRepository { Page foo(Pageable pageable);