En JPA 2, usando CriteriaQuery, cómo contar los resultados

114

Soy bastante nuevo en JPA 2 y es CriteriaBuilder / CriteriaQuery API:

CriteriaQuery javadoc

CriteriaQuery en el tutorial de Java EE 6

Me gustaría contar los resultados de una CriteriaQuery sin recuperarlos realmente. Si es posible, no encontré ningún método de este tipo, la única forma sería hacer esto:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();

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

// initialize predicates here

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

Y esa no puede ser la forma correcta de hacerlo ...

¿Existe una solución?

Sean Patrick Floyd
fuente
Sería muy útil si alguien pudiera ayudar o incluir en las respuestas a continuación. ¿Cómo lograr la siguiente consulta de recuento utilizando la API de criterios JPA? seleccione el recuento (distinto col1, col2, col3) de my_table;
Bhavesh
buscando la respuesta a continuación, pero en lugar de qb.count use qb.distinctCount @Bhavesh
Tonino

Respuestas:

220

MyEntityVolverá una consulta de tipo MyEntity. Quieres una consulta para un 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();

Obviamente, querrá construir su expresión con las restricciones y agrupaciones, etc., que omitió en el ejemplo.

Affe
fuente
3
Eso es lo que me había imaginado yo mismo, gracias. Pero eso significa que no puedo usar la misma instancia de consulta para consultar el número de resultados y los resultados reales que sé que son análogos a SQL, pero que haría que esta API sea mucho más parecida a OOP. Bueno, al menos puedo reutilizar algunos de los predicados, supongo.
Sean Patrick Floyd
6
@Barett, si es un recuento bastante grande, probablemente no desee cargar una lista de cientos o miles de entidades en la memoria solo para averiguar cuántas hay.
Affe
@Barett esto se usa mucho en caso de paginación. De ahí la necesidad de un número total y solo un subconjunto de las filas reales.
gkephorus
2
Tenga en cuenta que qb.countse realiza sobre el Root<MyEntity>de su consulta ( Root<MyEntity>myEntity = cq.from (MyEntity.class)) y esto a menudo ya está en su código de selección normal y cuando lo olvida, termina con una unión a sí mismo.
gkephorus
2
Para reutilizar los mismos criterios para la recuperación de objetos y el recuento, es posible que deba usar alias en la raíz, consulte forum.hibernate.org/viewtopic.php?p=2471522#p2471522 para ver un ejemplo.
Piscina
31

He resuelto esto usando cb.createQuery () (sin el parámetro de tipo de resultado):

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

Espero eso ayude :)

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

return em.createQuery(cq).getSingleResult();
axtavt
fuente
12

Como otras respuestas son correctas, pero demasiado simples, para que sea más completo, presento el siguiente fragmento de código para realizar SELECT COUNTen una consulta sofisticada de criterios JPA (con múltiples combinaciones, recuperaciones, condiciones).

Se modifica ligeramente esta respuesta .

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);
    }
}

Espero que le ahorre tiempo a alguien.

Porque en mi humilde opinión, la API de criterios JPA no es intuitiva ni del todo legible.

G. Demecki
fuente
2
@specializt, por supuesto, no es perfecto; por ejemplo, la solución anterior todavía falta una combinación recursiva en las recuperaciones. ¿Pero crees que solo por esto no debería compartir mis pensamientos? En mi humilde opinión, compartir conocimientos es la idea principal detrás de StackOverfow.
G. Demecki
La recursividad en las bases de datos es siempre la peor solución imaginable ... eso es un error de principiantes.
specializt
@specializt recursion on databases? Estaba hablando de recursividad a nivel de API. No confunda estos conceptos :-) JPA viene con una API muy poderosa / compleja que le permite hacer múltiples combinaciones / recuperaciones / agregaciones / alias, etc. en una sola consulta. Tienes que lidiar con eso mientras cuentas.
G. Demecki
1
Aparentemente, aún no ha entendido cómo funciona JPA: la gran mayoría de sus criterios se asignarán a las consultas de base de datos adecuadas, incluidas estas uniones (extremadamente extrañas). Active la salida SQL y observe su error - no hay una "capa API", JPA es una capa de ABSTRACCIÓN
especializt
lo más probable es que vea muchos JOIN en cascada, porque JPA todavía no puede crear funciones SQL automáticamente; pero eso cambiará en algún momento ... probablemente con JPA 3, recuerdo las discusiones sobre estas cosas
specializt
5

Es un poco complicado, dependiendo de la implementación de JPA 2 que use, esta funciona para EclipseLink 2.4.1, pero no para Hibernate, aquí un recuento genérico de CriteriaQuery para 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();
  }

El otro día migré de EclipseLink a Hibernate y tuve que cambiar mi función de conteo a la siguiente, así que siéntete libre de usarlo ya que este es un problema difícil de resolver, puede que no funcione para tu caso, ha estado en uso desde Hibernate 4.x, observe que no trato de adivinar cuál es la raíz, sino que la paso de la consulta para resolver el problema, demasiados casos de esquina ambiguos para intentar adivinar:

  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();
  }
Guido Medina
fuente
¿Qué pasa si la consulta tiene una (s) combinación (s)?
Dave
Creo que el único caso que sería peligroso es cuando tiene una combinación izquierda y la raíz elegida no es la entidad principal. De lo contrario, no importa, porque el recuento será el mismo independientemente de la entidad seleccionada. En cuanto a las entidades de unión a la izquierda, estoy bastante seguro de que la primera entidad en la selección es la de referencia, por ejemplo, si tiene estudiantes que abandonaron los cursos para unirse, entonces elegir al estudiante debería ser lo natural porque podría haber cursos que el estudiante no está inscrito.
Guido Medina
1
Si la consulta original es groupBy query, el resultado sería un recuento para cada grupo. Si podemos convertir una CriteriaQuery en una SubQuery, luego contamos la subconsulta, funcionaría en todos los casos. ¿Podemos hacer eso?
Dave
Hola @Dave, llegué a la misma conclusión que tú, la solución real sería poder transformar consultas en subconsultas, eso funcionaría para todos los casos, incluso para contar filas después de un groupBy. En realidad, parece que no puedo encontrar una razón de por qué las diferentes clases para CriteriaQuery y Subquery, o al menos el hecho de que la interfaz común que comparten, AbstractQuery, no define un método de selección. Debido a esto, no hay forma de reutilizar casi nada. ¿Ha encontrado una solución limpia para reutilizar una consulta agrupada por para contar las filas?
Amanda Tarafa Mas
1

También puede utilizar Proyecciones:

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

Long totalRows = (Long) criteria.list().get(0);
Pavel Evstigneev
fuente
1
Me temo que la API de proyecciones es específica de Hibernate, pero la pregunta es sobre JPA 2.
gersonZaragocin
Aún así, lo encuentro una adición útil, pero tal vez debería haber sido un comentario. ¿Puede ampliar su respuesta para incluir la respuesta completa específica de Hibernate?
Benny Bottema
gersonZaragocin está de acuerdo, pero no hay bloques de código en los comentarios
Pavel Evstigneev
0

Con Spring Data Jpa, podemos usar este método:

    /*
     * (non-Javadoc)
     * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#count(org.springframework.data.jpa.domain.Specification)
     */
    @Override
    public long count(@Nullable Specification<T> spec) {
        return executeCountQuery(getCountQuery(spec, getDomainClass()));
    }
kafkas
fuente