在JPA 2中,使用CriteriaQuery,如何计算结果数量?

146

我对JPA 2和它的CriteriaBuilder / CriteriaQuery API还比较陌生:

CriteriaQuery javadoc

CriteriaQuery在Java EE 6教程中的介绍

我想要统计一个CriteriaQuery的结果数量,但是不想实际获取这些结果。有这种可能吗?我没有找到任何类似的方法,唯一的方法是这样的:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();

CriteriaQuery<MyEntity> cq = cb
        .createQuery(MyEntityclass);

// initialize predicates here

return entityManager.createQuery(cq).getResultList().size();

这似乎不是正确的做法…

有解决方案吗?


如果有人能在下面的答案中提供帮助或解决方案,那将非常有用。如何使用JPA标准API实现以下计数查询?select count(distinct col1, col2, col3) from my_table; - Bhavesh
看下面的答案,但是用qb.distinctCount代替qb.count。@Bhavesh - Tonino
6个回答

270

查询类型是MyEntity,将返回MyEntity。您想要查询的是Long

CriteriaBuilder qb = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = qb.createQuery(Long.class);
cq.select(qb.count(cq.from(MyEntity.class)));
cq.where(/*your stuff*/);
return entityManager.createQuery(cq).getSingleResult();

显然,您将希望在示例中跳过的任何限制和分组等方面构建表达式。


3
我明白了,谢谢。但这意味着我不能使用同一查询实例来查询结果数量和实际结果,尽管这与SQL类似,但这将使该API更符合面向对象编程。好吧,至少我可以重用一些谓词。 - Sean Patrick Floyd
6
如果数量很大,你可能不想加载数百或数千个实体的列表到内存中,只是为了找出有多少个! - Affe
4
请注意,qb.count 是在查询的 Root<MyEntity> 上执行的(Root<MyEntity> myEntity = cq.from(MyEntity.class)),这通常已经包含在您的正常选择代码中,如果您忘记了,就会导致自连接。 - gkephorus
6
为了在检索对象和计数时重复使用相同的标准,您可能需要在根上使用别名,参见 https://forum.hibernate.org/viewtopic.php?p=2471522#p2471522 以获得示例。 - Pool
2
不带where条件对我无效。它给我返回表中的总记录数。忽略where条件,有什么原因吗? - S_K
显示剩余6条评论

36

我使用 cb.createQuery() 来解决了这个问题(没有结果类型参数):

public class Blah() {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery query = criteriaBuilder.createQuery();
    Root<Entity> root;
    Predicate whereClause;
    EntityManager entityManager;
    Class<Entity> domainClass;

    ... Methods to create where clause ...

    public Blah(EntityManager entityManager, Class<Entity> domainClass) {
        this.entityManager = entityManager;
        this.domainClass = domainClass;
        criteriaBuilder = entityManager.getCriteriaBuilder();
        query = criteriaBuilder.createQuery();
        whereClause = criteriaBuilder.equal(criteriaBuilder.literal(1), 1);
        root = query.from(domainClass);
    }

    public CriteriaQuery<Entity> getQuery() {
        query.select(root);
        query.where(whereClause);
        return query;
    }

    public CriteriaQuery<Long> getQueryForCount() {
        query.select(criteriaBuilder.count(root));
        query.where(whereClause);
        return query;
    }

    public List<Entity> list() {
        TypedQuery<Entity> q = this.entityManager.createQuery(this.getQuery());
        return q.getResultList();
    }

    public Long count() {
        TypedQuery<Long> q = this.entityManager.createQuery(this.getQueryForCount());
        return q.getSingleResult();
    }
}
希望能有所帮助 :)

27
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
cq.select(cb.count(cq.from(MyEntity.class)));

return em.createQuery(cq).getSingleResult();

难道不应该是 em.createQuery(cq).getResultList().stream().reduce(0L, Long::sum) 吗? - cnmuc

17

其他回答是正确的,但过于简单,为了完整性,我在下面提供代码片段来执行在一个复杂的 JPA Criteria 查询上进行 SELECT COUNT(包含多个连接、获取、条件)。

这是稍作修改的这个答案

public <T> long count(final CriteriaBuilder cb, final CriteriaQuery<T> selectQuery,
        Root<T> root) {
    CriteriaQuery<Long> query = createCountQuery(cb, selectQuery, root);
    return this.entityManager.createQuery(query).getSingleResult();
}

private <T> CriteriaQuery<Long> createCountQuery(final CriteriaBuilder cb,
        final CriteriaQuery<T> criteria, final Root<T> root) {

    final CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
    final Root<T> countRoot = countQuery.from(criteria.getResultType());

    doJoins(root.getJoins(), countRoot);
    doJoinsOnFetches(root.getFetches(), countRoot);

    countQuery.select(cb.count(countRoot));
    countQuery.where(criteria.getRestriction());

    countRoot.alias(root.getAlias());

    return countQuery.distinct(criteria.isDistinct());
}

@SuppressWarnings("unchecked")
private void doJoinsOnFetches(Set<? extends Fetch<?, ?>> joins, Root<?> root) {
    doJoins((Set<? extends Join<?, ?>>) joins, root);
}

private void doJoins(Set<? extends Join<?, ?>> joins, Root<?> root) {
    for (Join<?, ?> join : joins) {
        Join<?, ?> joined = root.join(join.getAttribute().getName(), join.getJoinType());
        joined.alias(join.getAlias());
        doJoins(join.getJoins(), joined);
    }
}

private void doJoins(Set<? extends Join<?, ?>> joins, Join<?, ?> root) {
    for (Join<?, ?> join : joins) {
        Join<?, ?> joined = root.join(join.getAttribute().getName(), join.getJoinType());
        joined.alias(join.getAlias());
        doJoins(join.getJoins(), joined);
    }
}

希望这能节省某些人的时间。

因为在我看来,JPA Criteria API 不够直观也不够易读。


3
@specializt 当然它并不完美 - 例如上面的解决方案仍然缺少递归连接。但是你认为仅仅因为这个原因我就不应该分享我的想法吗?在我看来,分享知识是StackOverflow背后的主要理念。 - G. Demecki
在数据库上使用递归总是最糟糕的解决方案,这是一个初学者的错误。 - specializt
@specializt 数据库递归?我在谈论API级别的递归。不要混淆这些概念 :-) JPA带有非常强大/复杂的API,允许您在单个查询中执行多个连接/获取/聚合/别名等操作。在计数时必须处理它。 - G. Demecki
1
显然,您还没有理解JPA的工作原理 - 您的大多数标准将被映射到适当的数据库查询中,包括这些(非常奇怪的)连接。激活SQL输出并观察您的错误 - 没有“API层”,JPA是一个抽象层。 - specializt
很可能,您会看到许多级联的JOIN - 因为JPA目前无法自动创建SQL函数;但这将在某个时候改变...可能是在JPA 3中,我记得有关于这些事情的讨论。 - specializt
显示剩余2条评论

5

这有点棘手,取决于您使用的JPA 2实现方式,这个适用于EclipseLink 2.4.1,但不适用于Hibernate。这里是适用于EclipseLink的通用CriteriaQuery计数:

public static Long count(final EntityManager em, final CriteriaQuery<?> criteria)
  {
    final CriteriaBuilder builder=em.getCriteriaBuilder();
    final CriteriaQuery<Long> countCriteria=builder.createQuery(Long.class);
    countCriteria.select(builder.count(criteria.getRoots().iterator().next()));
    final Predicate
            groupRestriction=criteria.getGroupRestriction(),
            fromRestriction=criteria.getRestriction();
    if(groupRestriction != null){
      countCriteria.having(groupRestriction);
    }
    if(fromRestriction != null){
      countCriteria.where(fromRestriction);
    }
    countCriteria.groupBy(criteria.getGroupList());
    countCriteria.distinct(criteria.isDistinct());
    return em.createQuery(countCriteria).getSingleResult();
  }

前几天,我从EclipseLink迁移到Hibernate,并不得不将我的计数函数更改为以下内容。因此,您可以自由使用其中任何一个,因为这是一个难以解决的问题,它可能无法适用于您的情况,自Hibernate 4.x以来一直在使用。请注意,我不尝试猜测哪个是根,而是从查询中传递它,因此问题得到了解决,有太多模棱两可的角落要猜测:

  public static <T> long count(EntityManager em,Root<T> root,CriteriaQuery<T> criteria)
  {
    final CriteriaBuilder builder=em.getCriteriaBuilder();
    final CriteriaQuery<Long> countCriteria=builder.createQuery(Long.class);

    countCriteria.select(builder.count(root));

    for(Root<?> fromRoot : criteria.getRoots()){
      countCriteria.getRoots().add(fromRoot);
    }

    final Predicate whereRestriction=criteria.getRestriction();
    if(whereRestriction!=null){
      countCriteria.where(whereRestriction);
    }

    final Predicate groupRestriction=criteria.getGroupRestriction();
    if(groupRestriction!=null){
      countCriteria.having(groupRestriction);
    }

    countCriteria.groupBy(criteria.getGroupList());
    countCriteria.distinct(criteria.isDistinct());
    return em.createQuery(countCriteria).getSingleResult();
  }

如果查询中有联接,该怎么办? - Dave
我认为唯一危险的情况是当你使用左连接并且选择的根实体不是主实体时。否则,无论选择哪个实体,计数都将相同。至于左连接实体,我非常确定选择的第一个实体是参考实体,例如,如果你有学生左连接课程,那么选择学生应该是自然的,因为可能存在学生未注册的课程。 - Guido Medina
1
如果原始查询是groupBy查询,则结果将为每个组一个计数。如果我们可以将CriteriaQuery转换为SubQuery,然后对子查询进行计数,那么它将在所有情况下都起作用。我们能做到吗? - Dave
嗨@Dave,我得出了与您相同的结论,真正的解决方案将是能够将查询转换为子查询,这将适用于所有情况,甚至在groupBy之后计算行数。实际上,我似乎找不到CriteriaQuery和Subquery的不同类别的原因,或者至少它们共享的公共接口AbstractQuery没有定义select方法的事实。因此,几乎没有办法重复使用任何东西。您是否找到了重用分组查询以计算行数的干净解决方案? - Amanda Tarafa Mas

0

你也可以使用投影:

ProjectionList projection = Projections.projectionList();
projection.add(Projections.rowCount());
criteria.setProjection(projection);

Long totalRows = (Long) criteria.list().get(0);

1
很抱歉,Projections API 是特定于 Hibernate 的,但是问题是关于 JPA 2 的。 - gersonZaragocin
尽管如此,我发现它是一个有用的补充,但也许应该作为注释。你能否扩展你的回答,包括完整的与Hibernate相关的回答? - Benny Bottema
gersonZaragocin同意,但是注释中没有代码块。 - Pavel Evstigneev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接