diff --git a/querydsl-core/src/main/java/com/querydsl/core/Fetchable.java b/querydsl-core/src/main/java/com/querydsl/core/Fetchable.java index 85fdef7e5..1df007572 100644 --- a/querydsl-core/src/main/java/com/querydsl/core/Fetchable.java +++ b/querydsl-core/src/main/java/com/querydsl/core/Fetchable.java @@ -78,14 +78,21 @@ public interface Fetchable { * {@link QueryResults#getLimit()}, because it will be more performant. Also, count queries cannot be * properly generated for all dialects. For example: in JPA count queries can't be generated for queries * that have multiple group by expressions or a having clause. + * Get the projection in {@link QueryResults} form. + * + * Use {@link #fetch()} instead if you do not need the total count of rows in the query result. * * @return results + * @see #fetch() */ QueryResults fetchResults(); /** * Get the count of matched elements * + * Note: not all QueryDSL modules might optimize fetchCount using a count query. + * An implementation is allowed to fall back to {@code fetch().size()}. + * * @return row count */ long fetchCount(); diff --git a/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java b/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java index c353efcce..b3eae741d 100644 --- a/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java +++ b/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java @@ -75,9 +75,42 @@ public abstract class AbstractJPAQuery> exte this.entityManager = em; } + /** + * {@inheritDoc} + * + * @deprecated {@code fetchCount} requires a count query to be computed. In {@code querydsl-sql}, this is done + * by wrapping the query in a subquery, like so: {@code SELECT COUNT(*) FROM (<original query>)}. Unfortunately, + * JPQL - the query language of JPA - does not allow queries to project from subqueries. As a result there isn't a + * universal way to express count queries in JPQL. Historically QueryDSL attempts at producing a modified query + * to compute the number of results instead. + * + * However, this approach only works for simple queries. Specifically + * queries with multiple group by clauses and queries with a having clause turn out to be problematic. This is because + * {@code COUNT(DISTINCT a, b, c)}, while valid SQL in most dialects, is not valid JPQL. Furthermore, a having + * clause may refer select elements or aggregate functions and therefore cannot be emulated by moving the predicate + * to the where clause instead. + * + * In order to support {@code fetchCount} for queries with multiple group by elements or a having clause, we + * generate the count in memory instead. This means that the method simply falls back to returning the size of + * {@link #fetch()}. For large result sets this may come at a severe performance penalty. + * + * For very specific domain models where {@link #fetchCount()} has to be used in conjunction with complex queries + * containing multiple group by elements and/or a having clause, we recommend using the + * Blaze-Persistence + * integration for QueryDSL. Among other advanced query features, Blaze-Persistence makes it possible to select + * from subqueries in JPQL. As a result the {@code BlazeJPAQuery} provided with the integration, implements + * {@code fetchCount} properly and always executes a proper count query. + */ @Override + @Deprecated public long fetchCount() { try { + if (getMetadata().getGroupBy().size() > 1 || getMetadata().getHaving() != null) { + logger.warn("Fetchable#fetchCount() was computed in memory! See the Javadoc for AbstractJPAQuery#fetchCount for more details."); + Query query = createQuery(null, false); + return query.getResultList().size(); + } + Query query = createQuery(null, true); return (Long) query.getSingleResult(); } finally { @@ -215,13 +248,53 @@ public abstract class AbstractJPAQuery> exte } } + /** + * {@inheritDoc} + * + * @deprecated {@code fetchResults} requires a count query to be computed. In {@code querydsl-sql}, this is done + * by wrapping the query in a subquery, like so: {@code SELECT COUNT(*) FROM (<original query>)}. Unfortunately, + * JPQL - the query language of JPA - does not allow queries to project from subqueries. As a result there isn't a + * universal way to express count queries in JPQL. Historically QueryDSL attempts at producing a modified query + * to compute the number of results instead. + * + * However, this approach only works for simple queries. Specifically + * queries with multiple group by clauses and queries with a having clause turn out to be problematic. This is because + * {@code COUNT(DISTINCT a, b, c)}, while valid SQL in most dialects, is not valid JPQL. Furthermore, a having + * clause may refer select elements or aggregate functions and therefore cannot be emulated by moving the predicate + * to the where clause instead. + * + * In order to support {@code fetchResults} for queries with multiple group by elements or a having clause, we + * generate the count in memory instead. This means that the method simply falls back to returning the size of + * {@link #fetch()}. For large result sets this may come at a severe performance penalty. + * + * For very specific domain models where {@link #fetchResults()} has to be used in conjunction with complex queries + * containing multiple group by elements and/or a having clause, we recommend using the + * Blaze-Persistence + * integration for QueryDSL. Among other advanced query features, Blaze-Persistence makes it possible to select + * from subqueries in JPQL. As a result the {@code BlazeJPAQuery} provided with the integration, implements + * {@code fetchResults} properly and always executes a proper count query. + * + * Mind that for any scenario where the count is not strictly needed separately, we recommend to use {@link #fetch()} + * instead. + */ @Override + @Deprecated public QueryResults fetchResults() { try { + QueryModifiers modifiers = getMetadata().getModifiers(); + if (getMetadata().getGroupBy().size() > 1 || getMetadata().getHaving() != null) { + logger.warn("Fetchable#fetchResults() was computed in memory! See the Javadoc for AbstractJPAQuery#fetchResults for more details."); + Query query = createQuery(null, false); + @SuppressWarnings("unchecked") + List resultList = query.getResultList(); + int offset = modifiers.getOffsetAsInteger() == null ? 0 : modifiers.getOffsetAsInteger(); + int limit = modifiers.getLimitAsInteger() == null ? resultList.size() : modifiers.getLimitAsInteger(); + return new QueryResults(resultList.subList(offset, Math.min(resultList.size(), offset + limit)), modifiers, resultList.size()); + } + Query countQuery = createQuery(null, true); long total = (Long) countQuery.getSingleResult(); if (total > 0) { - QueryModifiers modifiers = getMetadata().getModifiers(); Query query = createQuery(modifiers, false); @SuppressWarnings("unchecked") List list = (List) getResultList(query); diff --git a/querydsl-jpa/src/test/java/com/querydsl/jpa/JPABase.java b/querydsl-jpa/src/test/java/com/querydsl/jpa/JPABase.java index c3765498b..a99bf4ad8 100644 --- a/querydsl-jpa/src/test/java/com/querydsl/jpa/JPABase.java +++ b/querydsl-jpa/src/test/java/com/querydsl/jpa/JPABase.java @@ -29,6 +29,7 @@ import javax.persistence.EntityManager; import javax.persistence.FlushModeType; import javax.persistence.LockModeType; +import com.querydsl.core.QueryResults; import com.querydsl.core.Tuple; import org.junit.ClassRule; import org.junit.Ignore; @@ -279,4 +280,25 @@ public class JPABase extends AbstractJPATest implements JPATest { assertNotNull(row); } } + + @Test + public void fetchCountResultsGroupByWithMultipleFields() { + QueryResults results = query().from(cat) + .groupBy(cat.alive, cat.breed) + .select(cat.alive, cat.breed, cat.id.sum()) + .fetchResults(); + + assertEquals(1, results.getTotal()); + } + + @Test + public void fetchCountResultsGroupByWithHaving() { + QueryResults results = query().from(cat) + .groupBy(cat.alive) + .having(cat.id.sum().gt(5)) + .select(cat.alive, cat.id.sum()) + .fetchResults(); + + assertEquals(1, results.getTotal()); + } }