DATAJPA-86 - Pagination over queries using group-by works correctly.

Added handling of count methods for queries using group by. In case the count query returns multiple results we use the number of results instead of failing. If the result contains one result we use this one.

Moved hint handling to AbstractJpaQuery.
This commit is contained in:
Oliver Gierke
2011-09-04 16:56:50 +02:00
parent 170d31707c
commit 04349d25dc
10 changed files with 89 additions and 85 deletions

View File

@@ -17,12 +17,13 @@ package org.springframework.data.jpa.repository.query;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.QueryHint;
import javax.persistence.TypedQuery;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.CollectionExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.CollectionExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.PagedExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.PagedExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.SingleEntityExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.SingleEntityExecution;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -58,7 +59,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
* org.springframework.data.repository.query.RepositoryQuery#getQueryMethod * org.springframework.data.repository.query.RepositoryQuery#getQueryMethod
* () * ()
*/ */
public QueryMethod getQueryMethod() { public JpaQueryMethod getQueryMethod() {
return method; return method;
} }
@@ -106,12 +107,46 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
} }
} }
protected ParameterBinder createBinder(Object[] values) { /**
* Applies the declared query hints to the given query.
*
* @param query
* @return
*/
private <T extends Query> T applyHints(T query, JpaQueryMethod method) {
for (QueryHint hint : method.getHints()) {
query.setHint(hint.name(), hint.value());
}
return query;
}
protected ParameterBinder createBinder(Object[] values) {
return new ParameterBinder(getQueryMethod().getParameters(), values); return new ParameterBinder(getQueryMethod().getParameters(), values);
} }
protected abstract Query createQuery(Object[] values); protected Query createQuery(Object[] values) {
return applyHints(doCreateQuery(values), method);
}
protected abstract Query createCountQuery(Object[] values); protected TypedQuery<Long> createCountQuery(Object[] values) {
return applyHints(doCreateCountQuery(values), method);
}
/**
* Creates a {@link Query} instance for the given values.
*
* @param values must not be {@literal null}.
* @return
*/
protected abstract Query doCreateQuery(Object[] values);
/**
* Creates a {@link TypedQuery} for counting using the given values.
*
* @param values must not be {@literal null}.
* @return
*/
protected abstract TypedQuery<Long> doCreateCountQuery(Object[] values);
} }

View File

@@ -15,9 +15,12 @@
*/ */
package org.springframework.data.jpa.repository.query; package org.springframework.data.jpa.repository.query;
import java.util.List;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.TypedQuery;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
@@ -42,7 +45,6 @@ public abstract class JpaQueryExecution {
* @param binder * @param binder
* @return * @return
*/ */
public Object execute(AbstractJpaQuery query, Object[] values) { public Object execute(AbstractJpaQuery query, Object[] values) {
Assert.notNull(query); Assert.notNull(query);
@@ -71,7 +73,6 @@ public abstract class JpaQueryExecution {
@Override @Override
protected Object doExecute(AbstractJpaQuery query, Object[] values) { protected Object doExecute(AbstractJpaQuery query, Object[] values) {
return query.createQuery(values).getResultList(); return query.createQuery(values).getResultList();
} }
} }
@@ -93,11 +94,11 @@ public abstract class JpaQueryExecution {
protected Object doExecute(AbstractJpaQuery repositoryQuery, Object[] values) { protected Object doExecute(AbstractJpaQuery repositoryQuery, Object[] values) {
// Execute query to compute total // Execute query to compute total
Query projection = repositoryQuery.createCountQuery(values); TypedQuery<Long> projection = repositoryQuery.createCountQuery(values);
Long total = (Long) projection.getSingleResult(); List<Long> totals = projection.getResultList();
Long total = totals.size() == 1 ? totals.get(0) : totals.size();
Query query = repositoryQuery.createQuery(values); Query query = repositoryQuery.createQuery(values);
ParameterAccessor accessor = new ParametersParameterAccessor(parameters, values); ParameterAccessor accessor = new ParametersParameterAccessor(parameters, values);
return new PageImpl<Object>(query.getResultList(), accessor.getPageable(), total); return new PageImpl<Object>(query.getResultList(), accessor.getPageable(), total);

View File

@@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.query;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.TypedQuery;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -101,15 +102,10 @@ final class NamedQuery extends AbstractJpaQuery {
/* /*
* (non-Javadoc) * (non-Javadoc)
* * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateQuery(java.lang.Object[])
* @see
* org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery
* #
* createQuery(org.springframework.data.jpa.repository.query.ParameterBinder
* )
*/ */
@Override @Override
protected Query createQuery(Object[] values) { protected Query doCreateQuery(Object[] values) {
Query query = getEntityManager().createNamedQuery(queryName); Query query = getEntityManager().createNamedQuery(queryName);
return createBinder(values).bindAndPrepare(query); return createBinder(values).bindAndPrepare(query);
@@ -117,18 +113,15 @@ final class NamedQuery extends AbstractJpaQuery {
/* /*
* (non-Javadoc) * (non-Javadoc)
* * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(java.lang.Object[])
* @see
* org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery
* #createCountQuery(org.springframework.data.jpa.repository.query.
* ParameterBinder)
*/ */
@Override @Override
protected Query createCountQuery(Object[] values) { protected TypedQuery<Long> doCreateCountQuery(Object[] values) {
Query query = createQuery(values); Query query = createQuery(values);
String queryString = extractor.extractQueryString(query); String queryString = extractor.extractQueryString(query);
return createBinder(values).bind(getEntityManager().createQuery(QueryUtils.createCountQueryFor(queryString))); return createBinder(values).bind(
getEntityManager().createQuery(QueryUtils.createCountQueryFor(queryString), Long.class));
} }
} }

View File

@@ -95,7 +95,7 @@ public class ParameterBinder {
* @param query * @param query
* @return * @return
*/ */
public Query bind(Query query) { public <T extends Query> T bind(T query) {
int methodParameterPosition = 0; int methodParameterPosition = 0;
int queryParameterPosition = 1; int queryParameterPosition = 1;

View File

@@ -62,28 +62,23 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
/* /*
* (non-Javadoc) * (non-Javadoc)
* * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateQuery(java.lang.Object[])
* @see
* org.springframework.data.jpa.repository.query.AbstractJpaQuery#createQuery
* (javax.persistence.EntityManager,
* org.springframework.data.jpa.repository.query.ParameterBinder)
*/ */
@Override @Override
public Query createQuery(Object[] values) { public Query doCreateQuery(Object[] values) {
return query.createQuery(values); return query.createQuery(values);
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(java.lang.Object[])
* @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#
* createCountQuery(javax.persistence.EntityManager)
*/ */
@Override @Override
public Query createCountQuery(Object[] values) { @SuppressWarnings("unchecked")
public TypedQuery<Long> doCreateCountQuery(Object[] values) {
return countQuery.createQuery(values); return (TypedQuery<Long>) countQuery.createQuery(values);
} }
/** /**

View File

@@ -15,11 +15,9 @@
*/ */
package org.springframework.data.jpa.repository.query; package org.springframework.data.jpa.repository.query;
import java.util.List;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.QueryHint; import javax.persistence.TypedQuery;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -42,7 +40,6 @@ final class SimpleJpaQuery extends AbstractJpaQuery {
private final String queryString; private final String queryString;
private final String countQuery; private final String countQuery;
private final String alias; private final String alias;
private final List<QueryHint> hints;
private final Parameters parameters; private final Parameters parameters;
/** /**
@@ -53,7 +50,6 @@ final class SimpleJpaQuery extends AbstractJpaQuery {
super(method, em); super(method, em);
this.queryString = queryString; this.queryString = queryString;
this.alias = QueryUtils.detectAlias(queryString); this.alias = QueryUtils.detectAlias(queryString);
this.hints = method.getHints();
this.parameters = method.getParameters(); this.parameters = method.getParameters();
this.countQuery = method.getCountQuery() == null ? QueryUtils.createCountQueryFor(queryString) : method this.countQuery = method.getCountQuery() == null ? QueryUtils.createCountQueryFor(queryString) : method
.getCountQuery(); .getCountQuery();
@@ -64,48 +60,26 @@ final class SimpleJpaQuery extends AbstractJpaQuery {
/* /*
* (non-Javadoc) * (non-Javadoc)
* * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#createQuery(java.lang.Object[])
* @see
* org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery
* #
* createQuery(org.springframework.data.jpa.repository.query.ParameterBinder
* )
*/ */
@Override @Override
public Query createQuery(Object[] values) { public Query doCreateQuery(Object[] values) {
ParameterAccessor accessor = new ParametersParameterAccessor(parameters, values); ParameterAccessor accessor = new ParametersParameterAccessor(parameters, values);
String sortedQueryString = QueryUtils.applySorting(queryString, accessor.getSort(), alias); String sortedQueryString = QueryUtils.applySorting(queryString, accessor.getSort(), alias);
Query query = getEntityManager().createQuery(sortedQueryString); Query query = getEntityManager().createQuery(sortedQueryString);
return createBinder(values).bindAndPrepare(applyHints(query)); return createBinder(values).bindAndPrepare(query);
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(java.lang.Object[])
* @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#
* createCountQuery(java.lang.Object[])
*/ */
@Override @Override
protected Query createCountQuery(Object[] values) { protected TypedQuery<Long> doCreateCountQuery(Object[] values) {
return createBinder(values).bind(applyHints(getEntityManager().createQuery(countQuery))); return createBinder(values).bind(getEntityManager().createQuery(countQuery, Long.class));
}
/**
* Applies the declared query hints to the given query.
*
* @param query
* @return
*/
private Query applyHints(Query query) {
for (QueryHint hint : hints) {
query.setHint(hint.name(), hint.value());
}
return query;
} }
/** /**

View File

@@ -725,6 +725,18 @@ public class UserRepositoryTests {
assertThat(result, hasItem(firstUser)); assertThat(result, hasItem(firstUser));
} }
/**
* @see DATADOC-86
*/
@Test
public void readsPageWithGroupByClauseCorrectly() {
flushTestUsers();
Page<String> result = repository.findByLastnameGrouped(new PageRequest(0, 10));
assertThat(result.getTotalPages(), is(1));
}
private Page<User> executeSpecWithSort(Sort sort) { private Page<User> executeSpecWithSort(Sort sort) {
flushTestUsers(); flushTestUsers();

View File

@@ -30,7 +30,7 @@ import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution;
/** /**
* Unit test for {@link QueryExecution}. * Unit test for {@link JpaQueryExecution}.
* *
* @author Oliver Gierke * @author Oliver Gierke
*/ */

View File

@@ -23,8 +23,7 @@ import static org.mockito.Mockito.*;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.Query; import javax.persistence.TypedQuery;
import javax.persistence.QueryHint;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -34,7 +33,6 @@ import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner; import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.Parameters;
@@ -54,31 +52,22 @@ public class SimpleJpaQueryUnitTests {
@Mock @Mock
QueryExtractor extractor; QueryExtractor extractor;
@Mock @Mock
Query query; TypedQuery<Long> query;
@Mock @Mock
RepositoryMetadata metadata; RepositoryMetadata metadata;
@Mock @Mock
ParameterBinder binder; ParameterBinder binder;
@Before @Before
@QueryHints(@QueryHint(name = "foo", value = "bar"))
public void setUp() throws SecurityException, NoSuchMethodException { public void setUp() throws SecurityException, NoSuchMethodException {
when(em.createQuery(anyString())).thenReturn(query); when(em.createQuery(anyString())).thenReturn(query);
when(em.createQuery(anyString(), eq(Long.class))).thenReturn(query);
Method setUp = UserRepository.class.getMethod("findByLastname", String.class); Method setUp = UserRepository.class.getMethod("findByLastname", String.class);
method = new JpaQueryMethod(setUp, metadata, extractor); method = new JpaQueryMethod(setUp, metadata, extractor);
} }
@Test
public void appliesHintsCorrectly() throws Exception {
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "foobar");
jpaQuery.createQuery(new Object[] { "gierke" });
verify(query).setHint("foo", "bar");
}
@Test @Test
public void prefersDeclaredCountQueryOverCreatingOne() throws Exception { public void prefersDeclaredCountQueryOverCreatingOne() throws Exception {
@@ -86,7 +75,7 @@ public class SimpleJpaQueryUnitTests {
when(method.getCountQuery()).thenReturn("foo"); when(method.getCountQuery()).thenReturn("foo");
when(method.getParameters()).thenReturn( when(method.getParameters()).thenReturn(
new Parameters(SimpleJpaQueryUnitTests.class.getMethod("prefersDeclaredCountQueryOverCreatingOne"))); new Parameters(SimpleJpaQueryUnitTests.class.getMethod("prefersDeclaredCountQueryOverCreatingOne")));
when(em.createQuery("foo")).thenReturn(query); when(em.createQuery("foo", Long.class)).thenReturn(query);
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u"); SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u");

View File

@@ -185,4 +185,9 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
List<User> findByLastnameAndFirstnameAllIgnoringCase(String lastname, String firstname); List<User> findByLastnameAndFirstnameAllIgnoringCase(String lastname, String firstname);
/**
* @see DATADOC-86 - Count execution for group by
*/
@Query("select u.lastname from User u group by u.lastname")
Page<String> findByLastnameGrouped(Pageable pageable);
} }