No JPA 2, usando um CriteriaQuery, como contar os resultados

114

Sou bastante novo no JPA 2 e em sua API CriteriaBuilder / CriteriaQuery:

CriteriaQuery Javadoc

CriteriaQuery no tutorial Java EE 6

Eu gostaria de contar os resultados de um CriteriaQuery sem realmente recuperá-los. Isso é possível, não encontrei tal método, a única maneira seria fazer isso:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();

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

// initialize predicates here

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

E essa não pode ser a maneira correta de fazer isso ...

Há uma solução?

Sean Patrick Floyd
fonte
Seria muito útil se alguém pudesse ajudar ou incluir nas respostas abaixo. Como obter a seguinte consulta de contagem usando a API de critérios JPA? selecione a contagem (distinta col1, col2, col3) de minha_tabela;
Bhavesh
olhando a resposta abaixo, mas em vez de qb.count, use qb.distinctCount @Bhavesh
Tonino

Respostas:

220

Uma consulta do tipo MyEntityvai retornar MyEntity. Você quer uma consulta para um 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, você desejará construir sua expressão com quaisquer restrições e agrupamentos, etc, que você ignorou no exemplo.

Affe
fonte
3
Isso é o que eu mesma descobri, obrigado. Mas isso significa que não posso usar a mesma instância de consulta para consultar o número de resultados e os resultados reais que sei que são análogos ao SQL, mas que tornariam esta API muito mais parecida com OOP. Bem, pelo menos posso reutilizar alguns dos predicados, eu acho.
Sean Patrick Floyd
6
@Barett se for uma contagem bastante grande, você provavelmente não vai querer carregar uma lista de centenas ou milhares de entidades na memória apenas para descobrir quantas existem!
Affe
@Barett é usado em caso de muita paginação. Daí a necessidade de um número total e apenas um subconjunto das linhas reais.
gkephorus
2
Lembre-se de que o qb.counté feito sobre a Root<MyEntity>sua consulta ( Root<MyEntity>myEntity = cq.from (MyEntity.class)) e isso geralmente já está em seu código de seleção normal e quando você se esquece, termina com uma junção a self.
gkephorus
2
Para reutilizar os mesmos critérios para a recuperação de objetos e a contagem, você pode precisar usar aliases na raiz, consulte forum.hibernate.org/viewtopic.php?p=2471522#p2471522 para um exemplo.
Pool de
31

Eu resolvi isso usando o cb.createQuery () (sem o 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 que ajude :)

Reyiyo
fonte
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
fonte
12

Como outras respostas estão corretas, mas muito simples, para completar, estou apresentando abaixo o trecho de código para executar SELECT COUNTem um sofisticado consulta de critérios JPA (com várias junções, buscas, condições).

É ligeiramente modificado esta resposta .

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 economize o tempo de alguém.

Porque IMHO JPA Criteria API não é intuitiva nem totalmente legível.

G. Demecki
fonte
2
@specializt é claro que não é perfeito - por exemplo, a solução acima ainda não possui junções recursivas nas buscas. Mas você acha que só por causa disso eu não deveria compartilhar meus pensamentos? IMHO compartilhar conhecimento é a ideia principal por trás do StackOverfow.
G. Demecki
A recursão em bancos de dados é sempre a pior solução possível imaginável ... isso é um erro de iniciante.
specializt
@specializt recursion on databases? Eu estava falando sobre recursão no nível da API. Não confunda esses conceitos :-) JPA vem com uma API muito poderosa / complexa que permite que você faça várias junções / buscas / agregações / aliases etc em uma única consulta. Você tem que lidar com isso enquanto conta.
G. Demecki
1
Aparentemente, você ainda não entendeu como funciona o JPA - a grande maioria de seus critérios será mapeada em consultas de banco de dados apropriadas, incluindo essas junções (extremamente estranhas). Ative a saída SQL e observe seu erro - não há "camada API", JPA é uma camada ABSTRACTION
specializt
muito provavelmente, você verá muitos JOINs em cascata - porque o JPA ainda não pode criar funções SQL automaticamente; mas isso vai mudar algum dia ... provavelmente com JPA 3, lembro-me de discussões sobre essas coisas
specializt
5

É um pouco complicado, dependendo da implementação JPA 2 que você usa, este funciona para EclipseLink 2.4.1, mas não para Hibernate, aqui uma contagem de CriteriaQuery genérica 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();
  }

Outro dia eu migrei do EclipseLink para o Hibernate e tive que mudar minha função de contagem para a seguinte, então sinta-se à vontade para usar, pois este é um problema difícil de resolver, pode não funcionar para o seu caso, está em uso desde o Hibernate 4.x, observe que não tento adivinhar qual é a raiz, em vez disso, passo a partir da consulta para que o problema seja resolvido, muitos casos ambíguos para tentar adivinhar:

  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
fonte
e se a consulta tiver junção (ões)?
Dave
Acho que o único caso que seria perigoso é quando você tem uma junção à esquerda e a raiz escolhida não é a entidade principal. Caso contrário, não importa, porque a contagem será a mesma independentemente da entidade escolhida. Quanto às entidades left join, tenho certeza que a primeira entidade no select é a de referência, por exemplo, se você tem alunos que deixaram de ingressar nos cursos, escolher o aluno deve ser a coisa natural porque pode haver cursos que o aluno não é inscrito.
Guido Medina
1
Se a consulta original for groupBy query, o resultado seria uma contagem para cada grupo. Se pudermos transformar uma CriteriaQuery em uma Subconsulta e contar a subconsulta, ela funcionará em todos os casos. Podemos fazer isso?
Dave
Olá @Dave, Cheguei à mesma conclusão que você, a solução real seria poder transformar consultas em subconsultas, o que funcionaria para todos os casos, mesmo para contar linhas após um groupBy. Na verdade, não consigo encontrar uma razão para explicar por que as diferentes classes de CriteriaQuery e Subquery, ou pelo menos o fato de que a interface comum que elas compartilham, AbstractQuery, não define um método de seleção. Por isso, não há como reutilizar quase nada. Você encontrou uma solução limpa para reutilizar um agrupado por consulta para contar as linhas?
Amanda Tarafa Mas
1

Você também pode usar as projeções:

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

Long totalRows = (Long) criteria.list().get(0);
Pavel Evstigneev
fonte
1
Receio que a API de projeções seja específica do Hibernate, mas a pergunta é sobre JPA 2.
gersonZaragocin
Mesmo assim, acho que é uma adição útil, mas talvez devesse ser um comentário. Você pode expandir sua resposta para incluir a resposta completa específica do Hibernate?
Benny Bottema
gersonZaragocin concorda, mas não há blocos de código nos comentários
Pavel Evstigneev
0

Com 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
fonte