ใน JPA 2 โดยใช้ CriteriaQuery วิธีนับผลลัพธ์


114

ฉันค่อนข้างใหม่กับ JPA 2 และเป็น CriteriaBuilder / CriteriaQuery API:

CriteriaQuery Javadoc

CriteriaQuery ในบทช่วยสอน Java EE 6

ฉันต้องการนับผลลัพธ์ของ CriteriaQuery โดยไม่ต้องดึงข้อมูลออกมาจริงๆ เป็นไปได้ไหมฉันไม่พบวิธีการดังกล่าววิธีเดียวที่จะทำสิ่งนี้:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();

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

// initialize predicates here

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

และนั่นไม่ใช่วิธีที่เหมาะสมที่จะทำ ...

มีวิธีแก้ไขไหม


จะมีประโยชน์มากหากมีใครสามารถช่วยเหลือหรือระบุคำตอบไว้ด้านล่าง จะบรรลุแบบสอบถามการนับต่อไปนี้โดยใช้ API เกณฑ์ JPA ได้อย่างไร เลือกจำนวน (col1 ที่แตกต่างกัน, col2, col3) จาก my_table;
Bhavesh

มองคำตอบด้านล่าง แต่แทนที่จะใช้ qb.count ให้ใช้ qb.distinctCount @Bhavesh
Tonino

คำตอบ:


220

แบบสอบถามประเภทเป็นไปเพื่อผลตอบแทนMyEntity MyEntityคุณต้องการแบบสอบถามสำหรับไฟล์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();

เห็นได้ชัดว่าคุณต้องการสร้างนิพจน์ของคุณด้วยข้อ จำกัด และการจัดกลุ่มอื่น ๆ ที่คุณข้ามไปในตัวอย่าง


3
นั่นคือสิ่งที่ฉันคิดด้วยตัวเองขอบคุณ แต่นั่นหมายความว่าฉันไม่สามารถใช้อินสแตนซ์แบบสอบถามเดียวกันเพื่อค้นหาจำนวนผลลัพธ์และผลลัพธ์จริงที่ฉันรู้ว่าคล้ายคลึงกับ SQL แต่จะทำให้ API นี้มีลักษณะคล้าย OOP มากขึ้น อย่างน้อยฉันก็สามารถนำเพรดิเคตบางส่วนกลับมาใช้ใหม่ได้
Sean Patrick Floyd

6
@Barett หากเป็นการนับที่ค่อนข้างใหญ่คุณอาจไม่ต้องการโหลดรายการหลายร้อยหรือหลายพันรายการลงในหน่วยความจำเพียงเพื่อดูว่ามีจำนวนเท่าใด!
Affe

@Barett นี้ใช้ในกรณีที่มีการแบ่งหน้าเป็นจำนวนมาก ดังนั้นความต้องการจำนวนทั้งหมดและเพียงส่วนย่อยของแถวจริง
gkephorus

2
โปรดทราบว่าสิ่งqb.countนี้เสร็จสิ้นแล้วในRoot<MyEntity>ข้อความค้นหาของคุณ ( Root<MyEntity>myEntity = cq.from (MyEntity.class)) และสิ่งนี้มักจะอยู่ในรหัสเลือกปกติของคุณแล้วและเมื่อคุณลืมคุณจะต้องเข้าร่วมด้วยตนเอง
gkephorus

2
หากต้องการใช้เกณฑ์เดียวกันนี้ซ้ำสำหรับการดึงอ็อบเจ็กต์และการนับคุณอาจต้องใช้นามแฝงบนรูทโปรดดูที่forum.hibernate.org/viewtopic.php?p=2471522#p2471522สำหรับตัวอย่าง
สระว่ายน้ำ

31

ฉันได้จัดเรียงสิ่งนี้โดยใช้ 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();
    }
}

หวังว่าจะช่วยได้ :)


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

12

ในฐานะที่เป็นคำตอบของคนอื่น ๆ ที่ถูกต้อง แต่ง่ายเกินไปดังนั้นเพื่อความสมบูรณ์ฉันนำเสนอด้านล่างโค้ดที่จะดำเนินการSELECT COUNTในที่มีความซับซ้อนแบบสอบถาม JPA Criteria ที่ (มีการรวมการดึงข้อมูลเงื่อนไขต่างๆ)

มันมีการแก้ไขเล็กน้อยคำตอบนี้

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

หวังว่าจะช่วยประหยัดเวลาของใครบางคน

เนื่องจาก IMHO JPA Criteria API ไม่ใช้งานง่ายและอ่านง่าย


2
@specializt แน่นอนว่ามันไม่สมบูรณ์แบบ - เช่นโซลูชันข้างต้นยังขาดการรวมแบบเรียกซ้ำในการดึงข้อมูล แต่คุณคิดว่าเพียงเพราะเหตุนี้ฉันจึงไม่ควรแบ่งปันความคิดของฉัน? IMHO แบ่งปันความรู้เป็นแนวคิดหลักเบื้องหลัง StackOverfow
G. Demecki

การเรียกซ้ำบนฐานข้อมูลมักเป็นวิธีแก้ปัญหาที่แย่ที่สุดเท่าที่จะเป็นไปได้ ... นั่นเป็นความผิดพลาดของผู้เริ่มต้น
specializt

@specializt recursion on databases? ฉันกำลังพูดถึงการเรียกซ้ำในระดับ API อย่าสับสนแนวคิดเหล่านี้ :-) JPA มาพร้อมกับAPI ที่มีประสิทธิภาพ / ซับซ้อนมากซึ่งช่วยให้คุณสามารถทำการรวม / ดึง / การรวม / นามแฝงและอื่น ๆ ได้หลายรายการในแบบสอบถามเดียว คุณต้องจัดการกับมันในขณะที่นับ
G. Demecki

1
เห็นได้ชัดว่าคุณยังไม่เข้าใจว่า JPA ทำงานอย่างไร - เกณฑ์ส่วนใหญ่ของคุณจะถูกจับคู่กับแบบสอบถามฐานข้อมูลที่เหมาะสมรวมถึงการรวม (แปลกมาก) เหล่านี้ เปิดใช้งานเอาต์พุต SQL และสังเกตข้อผิดพลาดของคุณ - ไม่มี "เลเยอร์ API" JPA เป็นเลเยอร์ ABSTRACTION
specializt

เป็นไปได้มากว่าคุณจะเห็น JOIN แบบเรียงซ้อนจำนวนมาก - เนื่องจาก JPA ยังไม่สามารถสร้างฟังก์ชัน SQL โดยอัตโนมัติได้ แต่จะมีการเปลี่ยนแปลงในบางครั้ง ... อาจเป็นด้วย JPA 3 ฉันจำได้ว่ามีการพูดคุยเกี่ยวกับสิ่งเหล่านี้
specializt

5

มันค่อนข้างยุ่งยากขึ้นอยู่กับการใช้งาน JPA 2 ที่คุณใช้อันนี้ใช้ได้กับ EclipseLink 2.4.1 แต่ใช้ไม่ได้กับ Hibernate นี่คือ CriteriaQuery ทั่วไปสำหรับ 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 ไปที่ Hibernate และต้องเปลี่ยนฟังก์ชันการนับของฉันเป็นสิ่งต่อไปนี้ดังนั้นอย่าลังเลที่จะใช้อย่างใดอย่างหนึ่งเนื่องจากเป็นปัญหาที่ยากในการแก้ไขอาจใช้ไม่ได้กับกรณีของคุณมีการใช้งานตั้งแต่ไฮเบอร์เนต 4.x โปรดสังเกตว่าฉันไม่พยายามเดาว่าเป็นรูทใด แต่ฉันส่งผ่านจากแบบสอบถามเพื่อแก้ไขปัญหากรณีมุมที่คลุมเครือมากเกินไปที่จะพยายามเดา:

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

จะเกิดอะไรขึ้นถ้าข้อความค้นหาเข้าร่วม
Dave

ฉันคิดว่ากรณีเดียวที่อาจเป็นอันตรายคือเมื่อคุณมีการรวมด้านซ้ายและรูทที่เลือกไม่ใช่เอนทิตีหลัก มิฉะนั้นจะไม่สำคัญเพราะการนับจะเหมือนกันโดยไม่คำนึงถึงเอนทิตีที่เลือก สำหรับเอนทิตีการรวมด้านซ้ายฉันค่อนข้างแน่ใจว่าเอนทิตีแรกในการเลือกคือเอนทิตีอ้างอิงตัวอย่างเช่นหากคุณให้นักเรียนออกจากการเข้าร่วมหลักสูตรการเลือกนักเรียนควรเป็นเรื่องธรรมดาเพราะอาจมีหลักสูตรที่นักเรียนไม่ได้เป็น ที่ลงทะเบียนเรียน
Guido Medina

1
หากแบบสอบถามเดิมเป็นแบบสอบถาม groupBy ผลลัพธ์จะเป็นการนับหนึ่งสำหรับแต่ละกลุ่ม หากเราสามารถสร้าง CriteriaQuery เป็น SubQuery ได้จากนั้นให้นับเคียวรีย่อยก็จะใช้ได้ในทุกกรณี เราทำได้ไหม?
Dave

สวัสดี @Dave ฉันได้ข้อสรุปเช่นเดียวกับคุณวิธีแก้ปัญหาที่แท้จริงคือสามารถเปลี่ยนแบบสอบถามเป็นแบบสอบถามย่อยซึ่งจะใช้ได้กับทุกกรณีแม้กระทั่งการนับแถวหลังจาก groupBy อันที่จริงฉันไม่สามารถหาเหตุผลได้ว่าทำไม clases ที่แตกต่างกันสำหรับ CriteriaQuery และ Subquery หรือเนื่องจากอินเทอร์เฟซทั่วไปที่พวกเขาแชร์ AbstractQuery ไม่ได้กำหนดวิธีการเลือก ด้วยเหตุนี้จึงไม่มีวิธีนำเกือบทุกอย่างกลับมาใช้ใหม่ได้ คุณพบวิธีแก้ปัญหาที่สะอาดสำหรับการนำแบบสอบถามที่รวบรวมมาเพื่อนับแถวซ้ำหรือไม่?
Amanda Tarafa Mas

1

คุณยังสามารถใช้การคาดการณ์:

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

Long totalRows = (Long) criteria.list().get(0);

1
ฉันกลัวว่า Projections API เป็นไฮเบอร์เนตเฉพาะ แต่คำถามกำลังถามเกี่ยวกับ JPA 2
gersonZaragocin

ถึงกระนั้นฉันก็พบว่ามันมีประโยชน์ แต่บางทีมันควรจะเป็นความคิดเห็น คุณช่วยขยายคำตอบเพื่อรวมคำตอบเฉพาะไฮเบอร์เนตได้หรือไม่
Benny Bottema

gersonZaragocin เห็นด้วย แต่ไม่มีการบล็อกโค้ดในความคิดเห็น
Pavel Evstigneev

0

ด้วย Spring Data Jpa เราสามารถใช้วิธีนี้:

    /*
     * (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()));
    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.