Dans JPA 2, à l'aide d'un CriteriaQuery, comment compter les résultats

114

Je suis plutôt nouveau dans JPA 2 et c'est l'API CriteriaBuilder / CriteriaQuery:

CriteriaQuery javadoc

CriteriaQuery dans le didacticiel Java EE 6

Je voudrais compter les résultats d'une CriteriaQuery sans les récupérer. Est-ce possible, je n'ai trouvé aucune méthode de ce type, le seul moyen serait de le faire:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();

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

// initialize predicates here

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

Et cela ne peut pas être la bonne façon de le faire ...

Y a-t-il une solution?

Sean Patrick Floyd
la source
Il serait très utile que quelqu'un puisse aider ou inclure dans les réponses ci-dessous. Comment réaliser la requête de décompte suivante à l'aide de l'API de critères JPA? sélectionnez count (distinct col1, col2, col3) dans ma_table;
Bhavesh le
en regardant la réponse ci-dessous mais au lieu de qb.count, utilisez qb.distinctCount @Bhavesh
Tonino

Réponses:

220

Une requête de type MyEntityva revenir MyEntity. Vous voulez une requête pour 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();

De toute évidence, vous voudrez construire votre expression avec les restrictions et les regroupements, etc. que vous avez ignorés dans l'exemple.

Affe
la source
3
C'est ce que j'avais pensé moi-même, merci. Mais cela signifie que je ne peux pas utiliser la même instance de requête pour rechercher le nombre de résultats et les résultats réels qui, je le sais, sont analogues à SQL, mais qui rendraient cette API beaucoup plus semblable à la POO. Au moins, je peux réutiliser certains des prédicats, je suppose.
Sean Patrick Floyd
6
@Barett si c'est un nombre assez grand, vous ne voudrez probablement pas charger une liste de centaines ou de milliers d'entités en mémoire juste pour savoir combien il y en a!
Affe
@Barett ceci est beaucoup utilisé en cas de pagination. D'où la nécessité d'un nombre total et seulement d'un sous-ensemble des lignes réelles.
gkephorus
2
Rappelez-vous que le qb.countest fait sur le Root<MyEntity>de votre requête ( Root<MyEntity>myEntity = cq.from (MyEntity.class)) et cela est souvent déjà dans votre code de sélection normal et lorsque vous oubliez, vous vous retrouvez avec une jointure à soi.
gkephorus
2
Pour réutiliser les mêmes critères pour la récupération des objets et le décompte, vous devrez peut-être utiliser des alias à la racine, voir forum.hibernate.org/viewtopic.php?p=2471522#p2471522 pour un exemple.
Piscine du
31

J'ai trié cela en utilisant le cb.createQuery () (sans le paramètre de type de résultat):

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

J'espère que ça aide :)

Reyiyo
la source
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
la source
12

Comme les autres réponses sont correctes, mais trop simples, je présente ci-dessous un extrait de code à exécuter SELECT COUNTsur une requête de critères JPA sophistiquée (avec plusieurs jointures, extractions, conditions).

Il est légèrement modifié cette réponse .

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

J'espère que cela fera gagner du temps à quelqu'un.

Parce que l'API IMHO JPA Criteria n'est pas intuitive ni tout à fait lisible.

G. Demecki
la source
2
@specializt bien sûr ce n'est pas parfait - par exemple, la solution ci-dessus manque toujours une jointure récursive sur les récupérations. Mais pensez-vous que juste à cause de cela, je ne devrais pas partager mes pensées? IMHO partage des connaissances est l'idée principale derrière StackOverfow.
G.Demecki
La récursivité sur les bases de données est toujours la pire solution possible imaginable ... c'est une erreur de débutant.
specializt le
@specializt recursion on databases? Je parlais de récursivité au niveau de l'API. Ne confondez pas ces concepts :-) JPA est livré avec une API très puissante / complexe qui vous permet de faire plusieurs jointures / récupérations / agrégations / alias, etc. en une seule requête. Vous devez y faire face en comptant.
G.Demecki le
1
Apparemment, vous n'avez pas encore compris comment fonctionne JPA - la grande majorité de vos critères seront mappés sur des requêtes de base de données appropriées, y compris ces jointures (extrêmement étranges). Activez la sortie SQL et observez votre erreur - il n'y a pas de "couche API", JPA est une couche ABSTRACTION
specializt
très probablement, vous verrez de nombreuses JOIN en cascade - car JPA ne peut pas encore créer automatiquement des fonctions SQL; mais cela changera un jour ... probablement avec JPA 3, je me souviens des discussions sur ces choses
specializt
5

C'est un peu délicat, selon l'implémentation JPA 2 que vous utilisez, celle-ci fonctionne pour EclipseLink 2.4.1, mais pas pour Hibernate, voici un décompte de CriteriaQuery générique pour 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();
  }

L'autre jour, j'ai migré d'EclipseLink vers Hibernate et j'ai dû changer ma fonction de comptage pour la suivante, alors n'hésitez pas à utiliser l'un ou l'autre car c'est un problème difficile à résoudre, cela pourrait ne pas fonctionner pour votre cas, il est utilisé depuis Hibernate 4.x, notez que je n'essaie pas de deviner quelle est la racine, je la transmets à partir de la requête, donc le problème est résolu, trop de cas d'angle ambigus à essayer de deviner:

  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
la source
et si la requête a des jointures?
Dave
Je pense que le seul cas qui serait dangereux est celui où vous avez une jointure gauche et que la racine choisie n'est pas l'entité principale. Sinon, cela n'a pas d'importance, car le décompte sera le même quelle que soit l'entité choisie. En ce qui concerne les entités de jointure gauche, je suis tout à fait sûr que la première entité de la sélection est la référence, par exemple, si vous avez des étudiants qui rejoignent des cours, la sélection d'un étudiant devrait être la chose naturelle car il pourrait y avoir des cours que l'étudiant n'est pas inscrit.
Guido Medina
1
Si la requête d'origine est une requête groupBy, le résultat serait un décompte pour chaque groupe. Si nous pouvons transformer une CriteriaQuery en une sous-requête, puis compter la sous-requête, cela fonctionnerait dans tous les cas. pouvons-nous faire cela?
Dave
Salut @Dave, je suis arrivé à la même conclusion que vous, la vraie solution serait de pouvoir transformer les requêtes en sous-requêtes, cela fonctionnerait dans tous les cas, même pour compter les lignes après un groupBy. En fait, je n'arrive pas à trouver une raison pour laquelle les différentes classes de CriteriaQuery et Subquery, ou au moins le fait que l'interface commune qu'ils partagent, AbstractQuery, ne définit pas une méthode de sélection. Pour cette raison, il n'y a aucun moyen de réutiliser presque tout. Avez-vous trouvé une solution propre pour réutiliser une requête groupée par pour compter les lignes?
Amanda Tarafa Mas
1

Vous pouvez également utiliser les projections:

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

Long totalRows = (Long) criteria.list().get(0);
Pavel Evstigneev
la source
1
J'ai peur que l'API Projections soit spécifique à Hibernate, mais la question concerne JPA 2.
gersonZaragocin
Pourtant, je trouve que c'est un ajout utile, mais peut-être que cela aurait dû être un commentaire. Pouvez-vous élargir votre réponse pour inclure la réponse complète spécifique à Hibernate?
Benny Bottema
gersonZaragocin d'accord, mais il n'y a pas de blocs de code dans les commentaires
Pavel Evstigneev
0

Avec Spring Data Jpa, nous pouvons utiliser cette méthode:

    /*
     * (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
la source