17

I want to do something like this:

select count(*) from (select ...)

(As it would be in SQL), but in JPA.

Any ideas on how I would do it?

Piotr
  • 4,813
  • 7
  • 35
  • 46

3 Answers3

35

I stumbled upon this issue as well. I would ultimately like to execute the following JPQL:

SELECT COUNT(u)
FROM (
   SELECT DISTINCT u
   FROM User u
   JOIN u.roles r
   WHERE r.id IN (1)
)

But this wasn't possible, also not with criteria API. Research taught that this was just a design limitation in JPA. The JPA spec states that subqueries are only supported in WHERE and HAVING clauses (and thus not in the FROM).

Rewriting the query in the following JPQL form:

SELECT COUNT(u)
FROM User u
WHERE u IN (
   SELECT DISTINCT u
   FROM User u
   JOIN u.roles r
   WHERE r.id IN (1)
)

using the JPA Criteria API like as follows:

CriteriaQuery<Long> query = cb.createQuery(Long.class);
Root<User> u = query.from(User.class);
Subquery<User> subquery = query.subquery(User.class);
Root<User> u_ = subquery.from(User.class);
subquery.select(u_).distinct(true).where(u_.join("roles").get("id").in(Arrays.asList(1L)));
query.select(cb.count(u)).where(cb.in(u).value(subquery));
Long count = entityManager.createQuery(query).getSingleResult();
// ...

has solved the functional requirement for me. This should also give you sufficient insight into solving your particular functional requirement.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
5

This should do the trick (If you want to use JPA criteria API):

CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();  
CriteriaQuery<Long> query = cb.createQuery(Long.class);

Root<Entity> root = query.from(Entity.class);

//Selecting the count
query.select(cb.count(root));

//Create your search criteria
Criteria criteria = ...

//Adding search criteria   
query.where(criteria);

Long count = getEntityManager().createQuery(query).getSingleResult();

On the other hand, if you want to use JP-QL, the following code should do the trick:

//Add the where condition to the end of the query
Query query = getEntityManager().createQuery("select count(*) from Entity entity where...")
Long count = query.getSingleResult();
Erich Schreiner
  • 2,038
  • 1
  • 14
  • 24
pkainulainen
  • 1,458
  • 1
  • 19
  • 51
  • 2
    Problem is that I wanted to get the count of returned rows of a subquery. It seems that it is not possible because subqueries can not be used like that in JPA – Piotr Mar 28 '11 at 06:58
  • 17
    This doesn't answer the concrete question at all. The OP is asking how to count the results of a subquery, not how to count the results of a query. – BalusC Aug 22 '12 at 15:29
  • It does not work if zero rows returned. Hibernate crushes with `javax.persistence.NoResultException` – Stanislau Listratsenka Sep 30 '20 at 18:09
4

Use the following snippet to count rows for a given Criteria Query:

public static Query createNativeCountQuery(EntityManager em, CriteriaQuery<?> criteriaQuery) {
  org.hibernate.query.Query<?> hibernateQuery = em.createQuery(criteriaQuery).unwrap(org.hibernate.query.Query.class);
  String hqlQuery = hibernateQuery.getQueryString();

  QueryTranslatorFactory queryTranslatorFactory = new ASTQueryTranslatorFactory();
  QueryTranslator queryTranslator = queryTranslatorFactory.createQueryTranslator(
    hqlQuery,
    hqlQuery,
    Collections.emptyMap(),
    em.getEntityManagerFactory().unwrap(SessionFactoryImplementor.class),
    null
  );
  queryTranslator.compile(Collections.emptyMap(), false);

  String sqlCountQueryTemplate = "select count(*) from (%s)";
  String sqlCountQuery = String.format(sqlCountQueryTemplate, queryTranslator.getSQLString());

  Query nativeCountQuery = em.createNativeQuery(sqlCountQuery);

  Map<Integer, Object> positionalParamBindings = getPositionalParamBindingsFromNamedParams(hibernateQuery);
  positionalParamBindings.forEach(nativeCountQuery::setParameter);

  return nativeCountQuery;
}

private static Map<Integer, Object> getPositionalParamBindingsFromNamedParams(org.hibernate.query.Query<?> hibernateQuery) {
  Map<Integer, Object> bindings = new HashMap<>();

  for (var namedParam : hibernateQuery.getParameterMetadata().getNamedParameters()) {
    for (int location : namedParam.getSourceLocations()) {
      bindings.put(location + 1, hibernateQuery.getParameterValue(namedParam.getName()));
    }
  }

  return bindings;
}
fliX
  • 773
  • 8
  • 24
  • it works except for enums and list params – Mourad Zouabi Mar 14 '21 at 19:13
  • Does not work for me: `ERROR: subquery in FROM must have an alias`. And if I add an alias manually to `sqlCountQueryTemplate` it will throw another error: `Invalid endian flag value encountered.` – T3rm1 Apr 29 '22 at 10:09
  • 1
    Second error was caused indeed by enum parameter. Workaround: `if (parameterValue.getClass().isEnum()) { parameterValue = parameterValue.toString(); }` But only works if enums are saved as string in db. – T3rm1 Apr 29 '22 at 11:53
  • For using it with List params, the criteria filter must be written as: `root.("ref").in()` instead of `criteriaBuilder.in(root.("ref")).value()` This way it seems to be working fine for me – lgdestro Jun 24 '22 at 18:18