JPA 2.0:任意CriteriaQuery的计数?

12

我正在尝试实现以下方便方法:

/**
 * Counts the number of results of a search.
 * @param criteria The criteria for the query.
 * @return The number of results of the query.
 */
public int findCountByCriteria(CriteriaQuery<?> criteria);

在Hibernate中,这是通过

criteria.setProjection(Projections.rowCount());

在JPA中,如何等效于上面的代码?我找到了许多简单的计数示例,但没有一个使用应该确定行数的CriteriaQuery。

编辑:

不幸的是,我发现@Pascal的答案不正确。问题非常微妙,只有在使用连接时才会出现:

// Same query, but readable:
// SELECT *
// FROM Brain b
// WHERE b.iq = 170

CriteriaQuery<Person> query = cb.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
Join<Object, Object> brainJoin = root.join("brain");
Predicate iqPredicate = cb.equal(brainJoin.<Integer>get("iq"), 170);
query.select(root).where(iqPredicate);

在调用findCountByCriteria(query)时,出现以下异常:

org.hibernate.hql.ast.QuerySyntaxException: Invalid path: 'generatedAlias1.iq' [select count(generatedAlias0) from xxx.tests.person.dom.Person as generatedAlias0 where generatedAlias1.iq=170]

还有其他方式可以提供 CountByCriteria 方法吗?


2
你找到解决方案了吗?我也遇到了同样的问题,不确定该如何继续。谢谢。 - Ittai
@Ittai:我非常确定这是不可能的。 - blubb
@Ittai:显然,这是可能的。请参见Jose Luis Martin的答案。我没有尝试过,但它看起来像是真正的解决方案。 - blubb
非常感谢您的提醒,我会在回头时仔细查看它。 - Ittai
7个回答

21

我编写了一个实用类JDAL JpaUtils来完成这个功能:

  • 统计结果: Long count = JpaUtils.count(em, criteriaQuery);
  • 复制CriteriaQueries: JpaUtils.copyCriteria(em, criteriaQueryFrom, criteriaQueryTo);
  • 获取count条件查询: CriteriaQuery<Long> countCriteria = JpaUtils.countCriteria(em, criteria)

等等...

如果您对源代码感兴趣,请参见JpaUtils.java


你应该提到你是该实用类的作者。除此之外,这个答案值得满分,我会在强制等待24小时后立即将赏金授予这个答案。谢谢! - blubb
@blubb 谢谢,我刚刚编辑了。虽然没有完全测试,但似乎可以处理复杂的条件。 - Jose Luis Martin
@JoseLuisMartin - 你能帮忙使用JPA标准API实现以下计数查询吗?SELECT COUNT(DISTINCT col1, col2, col3) FROM my_table; - Bhavesh

5

我使用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();
    }
}

我所做的是类似于 CriteriaBuilder 的构建器,您可以使用相同的条件限制构建查询,然后调用 list() 或 count()。希望能对您有所帮助 :)

3

以上解决方案都无法适用于EclipseLink 2.4.1,它们最终都以笛卡尔积的计数方式结束(N^2),这里有一个针对EclipseLink的小技巧,唯一的缺点是如果您选择的实体不止一个,我不知道会发生什么情况,它将尝试从CriteriaQuery的第一个Root进行计数,但此解决方案对于Hibernate无效(JDAL有效,但JDAL对于EclipseLink无效)。

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将谓词中的根添加到标准中。我只是为2.0版本找到了解决方法。谢谢! - Jose Luis Martin

2
你是否正在寻找类似这样的东西?
/**
 * Counts the number of results of a search.
 * 
 * @param criteria The criteria for the query.
 * @return The number of results of the query.
 */
public <T> Long findCountByCriteria(CriteriaQuery<?> criteria) {
    CriteriaBuilder builder = em.getCriteriaBuilder();

    CriteriaQuery<Long> countCriteria = builder.createQuery(Long.class);
    Root<?> entityRoot = countCriteria.from(criteria.getResultType());
    countCriteria.select(builder.count(entityRoot));
    countCriteria.where(criteria.getRestriction());

    return em.createQuery(countCriteria).getSingleResult();
}

您可以像这样使用它:
// a search based on the Criteria API
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Person> criteria = builder.createQuery(Person.class);
Root<Person> personRoot = criteria.from(Person.class);
criteria.select(personRoot);
Predicate personRestriction = builder.and(
    builder.equal(personRoot.get(Person_.gender), Gender.MALE),
    builder.equal(personRoot.get(Person_.relationshipStatus), RelationshipStatus.SINGLE)
);
criteria.where(personRestriction);
//...

// and to get the result count of the above query
Long count = findCountByCriteria(criteria);

PS:我不确定这是否是实现它的正确/最佳方式,仍在学习Criteria API...


@Simon:很高兴能帮到你。如果愿意的话可以接受答案(在左侧投票计数器下方的绿色勾号)。 - Pascal Thivent
不幸的是,这个解决方案只适用于简单的查询。(请参见问题的编辑说明,它太长了,无法在评论中显示。) - blubb

2
如果你想要像Spring Data的Page元素一样获得结果和所有元素的计数,你可以进行两个查询。你可以将条件与查询执行分离。
以按城市查找用户为例:
 public List<User> getUsers(int userid, String city, other values ...) {

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<User> q = cb.createQuery(User.class);
    Root<User> c = q.from(User.class);

    List<Predicate> conditions = createConditions(c, cb, userid, city, ...other values);
    List<User> users = em.createQuery(q.select(c).where(conditions.toArray(new Predicate[] {})).distinct(true))
            .setMaxResults(PAGE_ELEMENTS).setFirstResult(page * PAGE_ELEMENTS).getResultList();
    return users;
}

除了getUser方法外,您还可以构建一个计算元素数量的第二个方法。
public Long getElemCount(int userid,  String city, ...other values) {

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Long> q = cb.createQuery(Long.class);
    Root<Location> root = q.from(Location.class);

    List<Predicate> conditions = createConditions(root, cb, userid, page, city, filter, module, isActive);
    Long userCount = em.createQuery(q.select(cb.count(root)).where(conditions.toArray(new Predicate[] {})).distinct(true))
            .getSingleResult();

    return userCount;
}

createConditions方法将同时处理这两种情况,因此您不必为条件重复编写逻辑。
<T> List<Predicate> createConditions(Root<T> root, CriteriaBuilder cb, int userid, String city, ... other values) {

    Join<User, SecondEntity> usr = root.join("someField");
    // add joins as you wish

    /*
     * Build Conditions
     */
    List<Predicate> conditions = new ArrayList<>();

    conditions.add(cb.equal(root.get("id"), userid));

    if (!city.equals("")) {
       conditions.add(cb.like(...));
    }

   // some more conditions...

    return conditions;
}

在您的控制器中,您可以这样做: long elementCount = yourCriteriaClassInstance.getElementCount(...); List users = yourCriteriaClassInstance.getUsers(...)

0
整个标准查询的理念在于它们是强类型的。因此,每个解决方案,如果您使用原始类型(在CriteriaQuery或Root或Root中没有泛型)- 这些解决方案都违反了这个主要思想。我遇到了同样的问题,正在努力以“适当”的方式(与JPA2一起)解决它。

-3

我用Hibernate和Criteria API做了类似的事情。

public Long getRowsCount(List<Criterion> restrictions ) {
       Criteria criteria = getSession().createCriteria(ThePersistenclass.class);
       for (Criterion x : restrictions)
             criteria.add(x);
   return criteria.setProjection(Projections.rowCount()).uniqueResult();        

}

希望能够帮到你


我知道这是使用 Hibernate API 可能实现的(正如我在问题中所述)。 我只对使用 JPA 接口的替代方法感兴趣。 - blubb

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