2020年7月19日 星期日

使用GROUP BY與聚合(Aggregation)函式

本文為【Spring Boot情境式網站開發指南:使用Spring Data JPA、Spring Security、Spring Web Flow】一書的【 第4章 Criteria API入門】延續,完整範例程式碼可至出版社下載

聚合函式不一定套用在全部資料,也可以使用CriteriaBuilder的groupBy()方法予以分組再執行聚合函式,並可搭配CriteriaBuilder的having()方法過濾分組條件。

groupBy() & count()

當JPQL使用以下查詢時:
  1. SELECT e.dept, COUNT(e) FROM Employee e GROUP BY e.dept
對應的Criteria API用法如下:
  1. @Test
  2. public void Criteria_GroupBy4Count() {
  3. EntityManager em = emf.createEntityManager();
  4. CriteriaBuilder cb = em.getCriteriaBuilder();
  5. CriteriaQuery sql = cb.createQuery(Object[].class);
  6. Root emp = sql.from(Employee.class);
  7. // make sql
  8. sql.multiselect(emp.get(Employee_.dept), cb.count(emp));
  9. sql.groupBy(emp.get(Employee_.dept));
  10. // run sql
  11. TypedQuery typedQuery = em.createQuery(sql);
  12. List resultList = typedQuery.getResultList();
  13. em.close();
  14. List stringList = resultList.stream()
  15. .map(o -> Arrays.toString(o))
  16. .collect(Collectors.toList());
  17. stringList.forEach(System.out::println);
  18. assertThat(stringList, containsInAnyOrder("[IT, 2]", "[Sales, 1]", "[Admin, 2]"));
  19. }
【說明】
行8-9:

  1. 使用CriteriaBuilder的groupBy()指定分組的依據欄位;該欄位也要出現在multiselect()方法中。
  2. 分組的目的在使用count()方法計算各組數量。

行5:

  1. 查詢分組欄位與各分組數量共2個欄位,使用Object[]型態輸出。

groupBy() & avg()

當JPQL使用以下查詢時:
  1. SELECT e.dept, AVG(e.salary) FROM Employee e GROUP BY e.dept
對應的Criteria API用法如下範例程式碼行8-10。同前例,只是將count()改為avg(),且avg()只接受數字欄位:
  1. @Test
  2. public void Criteria_GroupBy4Average() {
  3. EntityManager em = emf.createEntityManager();
  4. CriteriaBuilder cb = em.getCriteriaBuilder();
  5. CriteriaQuery sql = cb.createQuery(Object[].class);
  6. Root emp = sql.from(Employee.class);
  7. // make sql
  8. sql.multiselect(emp.get(Employee_.dept), cb.avg(emp.get(Employee_.salary)));
  9. sql.groupBy(emp.get(Employee_.dept));
  10. // run sql
  11. TypedQuery typedQuery = em.createQuery(sql);
  12. List resultList = typedQuery.getResultList();
  13. em.close();
  14. List stringList = resultList.stream().map(o -> Arrays.toString(o))
  15. .collect(Collectors.toList());
  16. stringList.forEach(System.out::println);
  17. assertThat(stringList, containsInAnyOrder("[IT, 3250.0]", "[Sales, 2000.0]",
  18. "[Admin, 3000.0]"));
  19. }

MAX()、GROUP BY、HAVING

當JPQL使用以下查詢時:
  1. SELECT e.dept, MAX(e.salary) FROM Employee e
  2. GROUP BY e.dept
  3. HAVING e.dept IN ('IT', 'Admin')
對應的Criteria API用法如下:
  1. @Test
  2. public void Criteria_GroupByAndHaving() {
  3. EntityManager em = emf.createEntityManager();
  4. CriteriaBuilder cb = em.getCriteriaBuilder();
  5. CriteriaQuery sql = cb.createQuery(Object[].class);
  6. Root emp = sql.from(Employee.class);
  7. // make sql
  8. sql.multiselect(emp.get(Employee_.dept), cb.max(emp.get(Employee_.salary)));
  9. sql.groupBy(emp.get(Employee_.dept));
  10. sql.having( emp.get(Employee_.dept).in("IT", "Admin") );
  11. // run sql
  12. TypedQuery typedQuery = em.createQuery(sql);
  13. List resultList = typedQuery.getResultList();
  14. em.close();
  15. List stringList = resultList.stream().map(o -> Arrays.toString(o))
  16. .collect(Collectors.toList());
  17. stringList.forEach(System.out::println);
  18. assertThat(stringList, containsInAnyOrder("[IT, 3500.0]", "[Admin, 4000.0]"));
  19. }
【說明】
行10:
使用CriteriaBuilder的having()方法限定分組欄位dept只能是IT與Admin兩種值:emp.get(Employee_.dept)in("IT", "Admin"));
相似的情況,也可以再加上orderBy()方法排序輸出結果。當JPQL使用以下查詢時:
  1. SELECT NEW entity.DeptGroup(e.dept, COUNT(e.dept))
  2. FROM Employee e
  3. GROUP BY e.dept
  4. HAVING COUNT(e.dept) > 1
  5. ORDER BY COUNT(e.dept) DESC, e.dept ASC
對應的Criteria API用法如下,可以由JPQL_GroupByWithConstructor()驗證:
  1. @Test
  2. public void Criteria_GroupByWithConstructor() {
  3. EntityManager em = emf.createEntityManager();
  4. CriteriaBuilder cb = em.getCriteriaBuilder();
  5. CriteriaQuery sql = cb.createQuery(DeptGroup.class);
  6. Root emp = sql.from(Employee.class);
  7. // reusable column expressions
  8. Expression deptExp = emp.get(Employee_.dept);
  9. Expression countExp = cb.count(deptExp);
  10. // make sql
  11. sql.multiselect(deptExp, countExp);
  12. sql.groupBy(deptExp);
  13. sql.having(cb.gt(countExp, 1));
  14. sql.orderBy(cb.desc(countExp), cb.asc(deptExp));
  15. // run sql
  16. TypedQuery query = em.createQuery(sql);
  17. List resultList = query.getResultList();
  18. resultList.forEach(System.out::println);
  19. assertThat(resultList,
  20. contains(new DeptGroup("Admin", 2), new DeptGroup("IT", 2)));
  21. }
【說明】
行8-9: 

群組欄位emp.get(Employee_.dept)與套用聚合函式的欄位在建構SQL時會反覆使用,因此宣告變數以利重複使用:
Expression<String> deptExp = emp.get(Employee_.dept);
Expression<Long> countExp = cb.count(deptExp);
行10-14: 
建構SQL: sql.having(cb.gt(countExp, 1)):要求分組後的數量統計必須great than(gt) 1。
sql.orderBy(cb.desc(countExp), cb.asc(deptExp)):輸出時排序。

沒有留言:

張貼留言