W JPA 2, używając CriteriaQuery, jak zliczać wyniki

114

Jestem raczej nowy w JPA 2 i jest to interfejs API CriteriaBuilder / CriteriaQuery:

CriteriaQuery javadoc

CriteriaQuery w samouczku Java EE 6

Chciałbym policzyć wyniki CriteriaQuery bez ich pobierania. Czy to możliwe, nie znalazłem takiej metody, jedynym sposobem byłoby to zrobić:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();

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

// initialize predicates here

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

I to nie może być właściwy sposób, aby to zrobić ...

Czy jest jakieś rozwiązanie?

Sean Patrick Floyd
źródło
Byłoby bardzo przydatne, gdyby ktoś mógł pomóc lub zawrzeć poniższe odpowiedzi. Jak uzyskać następujące zapytanie zliczające za pomocą interfejsu API kryteriów JPA? wybierz liczbę (różne kol1, kol2, kol3) z my_table;
Bhavesh
patrząc na odpowiedź poniżej, ale zamiast qb.count użyj qb.distinctCount @Bhavesh
Tonino

Odpowiedzi:

220

Zapytanie typu MyEntityzamierza powrócić MyEntity. Chcesz zapytać o 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();

Oczywiście będziesz chciał zbudować swoją ekspresję z wszelkimi ograniczeniami, grupowaniami itp., Które pominąłeś w przykładzie.

Affe
źródło
3
Tak właśnie sobie wyobrażałem, dzięki. Ale to oznacza, że ​​nie mogę użyć tej samej instancji zapytania, aby zapytać o liczbę wyników, a rzeczywiste wyniki, o których wiem, są analogiczne do SQL, ale które sprawiłyby, że ten interfejs API byłby znacznie bardziej podobny do OOP. Cóż, przynajmniej mogę ponownie użyć niektórych predykatów, jak sądzę.
Sean Patrick Floyd
6
@Barett, jeśli jest to dość duża liczba, prawdopodobnie nie chcesz ładować listy setek lub tysięcy jednostek do pamięci tylko po to, aby dowiedzieć się, ile ich jest!
Affe
@Barett jest to często używane w przypadku paginacji. Stąd potrzeba całkowitej liczby i tylko podzbioru rzeczywistych wierszy.
gkephorus
2
Pamiętaj, że qb.countjest to wykonywane przez Root<MyEntity>twoje zapytanie ( Root<MyEntity>myEntity = cq.from (MyEntity.class)) i często jest to już w twoim normalnym kodzie wyboru, a kiedy zapomnisz, kończysz się złączeniem do siebie.
gkephorus
2
Aby ponownie użyć tych samych kryteriów do pobierania obiektów i liczby, może być konieczne użycie aliasów w katalogu głównym, zobacz przykład na forum.hibernate.org/viewtopic.php?p=2471522#p2471522 .
Basen
31

Rozwiązałem to za pomocą cb.createQuery () (bez parametru typu wyniku):

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

Mam nadzieję, że to pomoże :)

reyiyo
źródło
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
źródło
12

Ponieważ inne odpowiedzi są poprawne, ale zbyt proste, więc dla kompletności przedstawiam poniżej fragment kodu do wykonania SELECT COUNTw wyrafinowanym zapytaniu Kryteriów JPA (z wieloma połączeniami, pobraniami, warunkami).

Jest to nieco zmodyfikowana tę odpowiedź .

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

Mam nadzieję, że to oszczędza komuś czas.

Ponieważ interfejs IMHO JPA Criteria API nie jest intuicyjny ani całkiem czytelny.

G. Demecki
źródło
2
@specializt oczywiście nie jest idealne - np. w powyższym rozwiązaniu wciąż brakuje rekurencyjnych złączeń przy pobieraniu. Ale czy myślisz, że właśnie z tego powodu nie powinienem dzielić się swoimi przemyśleniami? Udostępnianie wiedzy IMHO to główna idea StackOverfow.
G. Demecki
Rekurencja w bazach danych jest zawsze najgorszym możliwym rozwiązaniem, jakie można sobie wyobrazić ... to błąd początkujących.
specializt
@specializt recursion on databases? Mówiłem o rekurencji na poziomie API. Nie myl tych pojęć :-) JPA jest dostarczany z bardzo wydajnym / złożonym interfejsem API, który pozwala na wykonywanie wielu połączeń / pobierania / agregacji / aliasów itp. W jednym zapytaniu. Musisz sobie z tym poradzić podczas liczenia.
G. Demecki
1
Najwyraźniej nie rozumiałeś jeszcze, jak działa JPA - zdecydowana większość kryteriów zostanie zmapowana na odpowiednie zapytania do bazy danych, w tym te (niezwykle dziwne) łączenia. Aktywuj wyjście SQL i obserwuj swój błąd - nie ma "warstwy API", JPA jest warstwą ABSTRACTION
specjalizacja
najprawdopodobniej zobaczysz wiele kaskadowych POŁĄCZEŃ - ponieważ JPA nie może jeszcze automatycznie tworzyć funkcji SQL; ale to się kiedyś zmieni ... prawdopodobnie z JPA 3, przypominam sobie dyskusje na ten temat
specializt
5

Jest to trochę trudne, w zależności od używanej implementacji JPA 2, ta działa dla EclipseLink 2.4.1, ale nie dla Hibernate, tutaj ogólna liczba CriteriaQuery dla 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();
  }

Niedawno przeprowadziłem migrację z EclipseLink do Hibernate i musiałem zmienić moją funkcję zliczania na następującą, więc nie krępuj się jej użyć, ponieważ jest to trudny problem do rozwiązania, może nie działać w twoim przypadku, był używany od czasu hibernacji 4.x, zauważ, że nie próbuję odgadnąć, który jest korzeń, zamiast tego przekazuję go z zapytania, więc problem został rozwiązany, zbyt wiele niejednoznacznych przypadków narożnych, aby spróbować odgadnąć:

  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
źródło
co się stanie, jeśli zapytanie ma złączenia?
Dave,
Myślę, że jedynym przypadkiem, który byłby niebezpieczny, jest sytuacja, w której masz lewe sprzężenie, a wybrany root nie jest głównym bytem. W przeciwnym razie nie ma to znaczenia, ponieważ liczba będzie taka sama niezależnie od wybranej jednostki. Jeśli chodzi o encje lewostronne, jestem całkiem pewien, że pierwsza jednostka w selekcji jest referencyjna, na przykład, jeśli masz uczniów, którzy opuścili kursy, to wybór ucznia powinien być rzeczą naturalną, ponieważ mogą istnieć kursy, których student nie jest zarejestrowany.
Guido Medina
1
Jeśli pierwotne zapytanie to zapytanie groupBy, wynikiem będzie jedna liczba dla każdej grupy. Jeśli uda nam się przekształcić CriteriaQuery w podzapytanie, a następnie policzyć podzapytanie, zadziała to we wszystkich przypadkach. Czy możemy to zrobić?
Dave,
Cześć @Dave, doszedłem do tego samego wniosku, co ty, prawdziwym rozwiązaniem byłoby przekształcenie zapytań w podzapytania, które działałyby we wszystkich przypadkach, nawet w przypadku liczenia wierszy po groupBy. Właściwie nie mogę znaleźć powodu, dla którego różne klasy CriteriaQuery i Subquery, a przynajmniej fakt, że wspólny interfejs, który mają, AbstractQuery, nie definiuje metody select. Z tego powodu nie ma możliwości ponownego wykorzystania prawie wszystkiego. Czy znalazłeś proste rozwiązanie do ponownego wykorzystania pogrupowanych według zapytania do liczenia wierszy?
Amanda Tarafa Mas
1

Możesz także użyć Projekcji:

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

Long totalRows = (Long) criteria.list().get(0);
Pavel Evstigneev
źródło
1
Obawiam się, że Projections API jest specyficzne dla Hibernate'a, ale pytanie dotyczy JPA 2.
gersonZaragocin
Mimo wszystko uważam to za przydatne uzupełnienie, ale może powinien był to być komentarz. Czy możesz rozszerzyć swoją odpowiedź, tak aby zawierała pełną odpowiedź dotyczącą Hibernate?
Benny Bottema
gersonZaragocin zgadza się, ale w komentarzach nie ma bloków kodu
Pavel Evstigneev
0

Z Spring Data Jpa możemy skorzystać z tej metody:

    /*
     * (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
źródło