2020年7月19日 星期日

LEFT OUTER JOIN

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

使用LEFT OUTER JOIN並擷取Employee所有欄位

當JPQL使用以下語句時:
SELECT DISTINCT e FROM Employee e 
       LEFT OUTER JOIN e.tasks t
對應的Criteria API用法如下:
@Test
public void Criteria_LeftJoin1() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    emp.join(Employee_.tasks, JoinType.LEFT);
    sql.select(emp)
        .distinct(true);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    assertThat(resultList, containsInAnyOrder(
            employee1, employee2, 
            employee3, employee4));
    assertThat(resultList, hasSize(4));
}
說明:
  • 行7:
emp.join(Employee_.tasks, JoinType.LEFT);等價於Employee e LEFT OUTER JOIN e.tasks t。方法join()未指定JoinType時預設為JoinType.INNER。本例為LEFT JOIN,需指定JoinType.LEFT。

使用LEFT OUTER JOIN擷取Employee的name與Task的description欄位

承前例,但查詢的欄位改為「e.name, t.name」。當JPQL使用以下語句時:
SELECT DISTINCT e.name, t.name 
    FROM Employee e 
     LEFT OUTER JOIN e.tasks t
對應的Criteria API用法如下:
@Test 
public void Criteria_LeftJoin2() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createTupleQuery();
    Root emp = sql.from(Employee.class);
    Join task = emp.join(Employee_.tasks, JoinType.LEFT);
    // task.on(cb.equal(task.get(Task_.name), "Denise"));
    sql.select(cb.tuple(emp.get(Employee_.name).alias("employeeName"),
                         task.get(Task_.name).alias("supervisor")))
        .distinct(true);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    List rows = resultList.stream()
            .map(t -> t.get("employeeName", String.class) + ", " + 
                        t.get("supervisor", String.class))
            .collect(Collectors.toList());
    rows.forEach(System.out::println);
    em.close();
    assertThat(rows, containsInAnyOrder(
            "Tim, Mike", 
            "Tim, Denise",
            "Mike, Rose",
            "Mike, Mike",
            "Jim, Denise", 
            "Jack, null"));
    assertThat(rows, hasSize(6));
    /*      
    assertThat(rows, containsInAnyOrder(
            "Jim, Denise", 
            "Jack, null", 
            "Mike, null",
            "Tim, Denise"));
    assertThat(rows, hasSize(4));
    */  
}
說明:

  • 行7:
因為必須查詢聯結表格Task的欄位,因此建立Join<Employee, Task> task的物件參考指向join()方法執行結果。
  • 行8:
可以藉由Join<Employee, Task> task的on()方法限定對聯結表格Task的查詢條件,對應的JPQL子句為「LEFT OUTER JOIN e.tasks t ON t.name='Denise'」。本行若取消comment,將改得到行29-34的結果;或改執行單元測試方方法Criteria_LeftJoinOn()驗證結果。
  • 行10:
指定要查詢Task的name欄位。

在WHERE敘述使用比較(Comparision)關鍵字:BETWEEN

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

BETWEEN數字區間

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE e.salary BETWEEN 2000 AND 4000  
對應的Criteria API用法如下:
@Test 
public void Criteria_BetweenLiteralNumbers() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  sql.select(emp)
      .where(cb.between(emp.get(Employee_.salary), 2000.0, 4000.0));
  List resultList = em.createQuery(sql).getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee1, employee2, employee4));
  assertThat(resultList, hasSize(3));
}

BETWEEN數字欄位與數字

當JPQL使用以下查詢時:
SELECT e FROM Employee e 
        INNER JOIN e.supervisor s 
            WHERE e.salary BETWEEN s.salary AND 4000
            ORDER BY e.salary
對應的Criteria API用法如下:
@Test 
public void Criteria_BetweenLiteralNumberAndJoinedColumn() {
   EntityManager em = emf.createEntityManager();
   CriteriaBuilder cb = em.getCriteriaBuilder();
   CriteriaQuery sql = cb.createQuery(Employee.class);
   Root emp = sql.from(Employee.class);
   Join empSupervisor= emp.join(Employee_.supervisor);
   sql.select(emp)
       .where( cb.between(emp.get(Employee_.salary), 
                         empSupervisor.get(Employee_.salary),   // start
                         cb.literal(4000.0)) );                   // end
   List resultList = em.createQuery(sql).getResultList();
   resultList.forEach(System.out::println);
   em.close();
   assertThat(resultList, contains(employee1, employee2));
   assertThat(resultList, hasSize(2));
}
【說明】
行7:
因為要between()起始邊界為self Join 類別Employee,因此必須宣告Join<Employee, Employee> empSupervisor指向join()方法呼叫後的回傳物件。 
行9-11:
使用CriteriaBuilder的between()方法時,第1個參數指定要查詢的欄位,第2個參數為區間起始邊界,第3個參數為區間結束邊界。

BETWEEN日期區間命名變數 

當JPQL使用以下查詢時:
SELECT e FROM Employee e 
        WHERE e.joinDate BETWEEN :startDate AND :endDate 
        ORDER BY e.joinDate
對應的Criteria API用法如下:
@Test 
public void Criteria_BetweenNamedParams() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  // prepare parameters
  ParameterExpression startDate = 
                    cb.parameter(Timestamp.class, "myStart");
  ParameterExpression endDate = 
                    cb.parameter(Timestamp.class, "myEnd");
  // create query                   
  sql.select(emp)
      .where(cb.between(emp.get(Employee_.joinDate), startDate, endDate))
      .orderBy(cb.desc(emp.get(Employee_.joinDate)));
  // run query and set parameter values
  TypedQuery typedQuery = em.createQuery(sql);
  typedQuery.setParameter("myStart", 
                            localToTimeStamp(LocalDate.of(1990, 1, 1)));
  typedQuery.setParameter("myEnd", 
                            localToTimeStamp(LocalDate.of(2011, 1, 1)));
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, contains(employee3, employee1));
  assertThat(resultList, hasSize(2));
}
【說明】
行8-10:
以ParameterExpression<Timestamp>建立欄位joinDate的命名變數:myStart。
行10-11:
以ParameterExpression<Timestamp>建立欄位joinDate的命名變數:myEnd。
行18-19:
以TypedQuery的setParameter()方法指定命名變數myStart並傳入參數值。

行20-21:
以TypedQuery的setParameter()方法指定命名變數myEnd並傳入參數值。

在WHERE敘述使用比較(Comparision)關鍵字:IN


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

【小標】BETWEEN數字區間
當JPQL使用以下查詢時:

SELECT e FROM Employee e WHERE e.salary IN (2000.0, 3000.0, 4000.0)
對應的Criteria API用法如下:
@Test 
public void Criteria_InExpression() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    Expression salary = emp.get(Employee_.salary);
    Expression inExp = salary.in(2000.0, 3000.0, 4000.0);
    sql.select(emp).where(inExp);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, 
                containsInAnyOrder(employee1, employee2, employee3));
    assertThat(resultList, hasSize(3));
}
行7: 
建立欄位表示式物件Expression<Double> salary = emp.get(Employee_.salary);
行8: 
呼叫Expression<Double>物件的in()方法,允許傳入個數變動的Double物件,本例為in(2000.0, 3000.0, 4000.0),並回傳Expression<Boolean>物件,將作為where()方法的輸入參數。

使用NOT IN於字面字串常量群組

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE e.name NOT IN ('Jim', 'Rose')
對應的Criteria API用法如下:
public void Criteria_NotInExpression() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    sql.select(emp) .where( emp.get(Employee_.name).in("Jim", "Rose").not() );
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, containsInAnyOrder(employee3, employee4));
    assertThat(resultList, hasSize(2));
}
行6:
承前單元測試方法Criteria_InExpression(),本例不在刻意建立欄位表示式物件Expression<String>,與呼叫in()方法後回傳的Expression<Boolean>物件,直接將兩者合併傳入方法where()中:where( emp.get(Employee_.name).in("Jim", "Rose").not() );並在in()方法後再呼叫not()方法,表示否定:NOT IN。

使用IN於命名變數

當JPQL使用以下查詢時:
    Query query = em.createQuery(
        "SELECT e FROM Employee e WHERE e.dept IN :deptNames");
    query.setParameter("deptNames", Arrays.asList("IT", "Sales", "HR"));
對應的Criteria API用法如下:
@Test
public void Criteria_ParameterExpression() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  ParameterExpression deptParam = 
                      cb.parameter(Collection.class, "deptNames");
  sql.select(emp) .where(emp.get(Employee_.dept).in(deptParam));
  TypedQuery typedQuery = em.createQuery(sql);
  typedQuery.setParameter("deptNames", Arrays.asList("IT", "Sales", "HR"));
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee1, employee2, employee4));
  assertThat(resultList, hasSize(3));
}
【說明】
行7-8:
以ParameterExpression<Collection>建立欄位dept的命名變數物件參考「deptParam」,其名稱為「deptNames」,注意參數型態為Collection。
行9:
欄位emp.get(Employee_.dept)的限定條件為in(deptParam);

行11:
以TypedQuery的setParameter()方法指定命名變數deptNames並傳入Collection物件Arrays.asList("IT", "Sales", "HR")作為參數值。

在WHERE敘述使用比較(Comparision)關鍵字:LIKE


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

使用LIKE搭配萬用字元「_」查詢
當JPQL使用以下查詢時:

SELECT e FROM Employee e WHERE e.salary LIKE '_500.0'
對應的Criteria API用法如下:
@Test 
public void Criteria_Like1() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    sql.select(emp)
        .where( cb.like( emp.get(Employee_.salary).as(String.class), "_500.0" ) );
    List resultList = em.createQuery(sql).getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, containsInAnyOrder(employee4));
    assertThat(resultList, hasSize(1));
}
行8:
使用CriteriaBuilder的like()方法,第2個欄位指定like的樣式,第1個參數指定欄位。本例因為salary欄位是數字,要樣式比對必須是字串欄位:Predicate like(Expression<String> x, String pattern);因此使用as(String.class)方法將salary數字欄位值轉換為字串:emp.get(Employee_.salary).as(String.class)

使用LIKE搭配萬用字元「%」查詢

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE e.name LIKE 'J%'
對應的Criteria API用法如下:
@Test 
public void Criteria_Like2() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    sql.select(emp)
        .where( cb.like( emp.get(Employee_.name), "J%" ) );
    List resultList = em.createQuery(sql).getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, containsInAnyOrder(employee1));
    assertThat(resultList, hasSize(1));
}

使用NOT LIKE搭配命名變數查詢

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE e.name NOT LIKE :nameStartsWith
對應的Criteria API用法如下:
@Test
public void Criteria_NotLikeNamedParams() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  ParameterExpression nameParam = 
          cb.parameter(String.class, "nameStartsWith");
  sql.select(emp)
      .where( cb.notLike(emp.get(Employee_.name), nameParam) );
  //  .where( cb.like(emp.get(Employee_.name), nameParam).not() );
  TypedQuery typedQuery = em.createQuery(sql);
  typedQuery.setParameter("nameStartsWith", "J%");
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee2, employee3, employee4));
  assertThat(resultList, hasSize(3));
}
行7-8:
以ParameterExpression< String >建立欄位name的命名變數物件參考「nameParam」,其名稱為「nameStartsWith」。
行10:
使用CriteriaBuilder的notLike()方法。欄位emp.get(Employee_.name)的限定條件為notLike (nameParam);
行11:
也可以使用CriteriaBuilder的like()方法,並在結尾加上not()。
行13:
以TypedQuery的setParameter()方法指定命名變數nameStartsWith並傳入字串樣式"J%"作為參數值。

使用LIKE並以ESCAPE跳脫萬用字元「_」查詢

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE e.dept LIKE '%@_%' ESCAPE '@'

對應的Criteria API用法如下:
@Test 
public void Criteria_LikeEscape() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    sql.select(emp)
        .where(cb.like(emp.get(Employee_.dept), "%@_%", '@'));
    List resultList = em.createQuery(sql).getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, containsInAnyOrder(employee4));
    assertThat(resultList, hasSize(1));
}
行8:
使用CriteriaBuilder的like()方法,因第2個參數要比對的是like樣式內的特殊字元「_」,因此由最後參數定義使用的跳脫字元:Predicate like(Expression<String> x, String pattern, char escapeChar);本例為「@」:cb.like( emp.get(Employee_.dept), "%@_%", '@' );

在WHERE敘述使用比較(Comparision)關鍵字:NULL


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

使用「isNull()」或「isNotNull()」可以查詢一般欄位物件值是否為null。

使用 IS NULL 查詢

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE e.dept IS NULL
對應的Criteria API用法如下:
@Test
public void Criteria_IsNull() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  sql.select(emp)
      .where(emp.get(Employee_.dept).isNull());
  //  .where(cb.equal(emp.get(Employee_.dept), cb.nullLiteral(String.class)));
  TypedQuery typedQuery = em.createQuery(sql);
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee4));
  assertThat(resultList, hasSize(1));
}
行8:
使用欄位表示式物件emp.get(Employee_.dept)的isNull()方法,等價於「e.dept IS NULL」
行9:
也可以使用CriteriaBuilder的equal()方法,第1個參數指定欄位dept,第2個參數則為CriteriaBuilder的nullLiteral(String.class)方法,等價於「e.dept = NULL」。

使用 IS NOT NULL 查詢

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE e.dept IS NOT NULL
對應的Criteria API用法如以下範例行8的isNotNull():
@Test
public void Criteria_IsNotNull() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  sql.select(emp)
      .where(emp.get(Employee_.dept).isNotNull());
  TypedQuery typedQuery = em.createQuery(sql);
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee1, employee2, employee3));
  assertThat(resultList, hasSize(3));
}

在WHERE敘述使用比較(Comparision)關鍵字:EMPTY

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

使用CriteriaBuilder的isEmpty()或isNotEmpty()方法可以查詢集合型態的物件欄位值是否為空(empty)。

使用 IS EMPTY 查詢

當JPQL使用以下查詢時:
SELECT e FROM Employee1 e WHERE e.tasks IS EMPTY
對應的Criteria API用法如下:
@Test
public void Criteria_JoinedEntityIsEmpty() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee1.class);
    Root emp1 = sql.from(Employee1.class);
    sql.select(emp1)
        .where( cb.isEmpty(emp1.get(Employee1_.tasks)) );
    List resultList = em.createQuery(sql).getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, containsInAnyOrder(employee4, employee2));
    assertThat(resultList, hasSize(2));
}
行8:
使用CriteriaBuilder的isEmpty()方法查詢集合型態的欄位Employee1_.tasks是否為空(empty)。

使用 IS NOT EMPTY 查詢

當JPQL使用以下查詢時:
SELECT e FROM Employee1 e WHERE e.tasks IS NOT EMPTY
對應的Criteria API用法如下: 
@Test
public void Criteria_JoinedEntityIsNotEmpty() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee1.class);
    Root emp1 = sql.from(Employee1.class);
    sql.select(emp1)
        .where( cb.isNotEmpty(emp1.get(Employee1_.tasks)) );
    List resultList = em.createQuery(sql).getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, contains(employee1, employee3));
    assertThat(resultList, hasSize(2));
}
行8:
使用CriteriaBuilder的isNotEmpty()方法查詢集合型態的欄位Employee1_.tasks是否不為空(empty)。

對一般欄位使用 IS NULL 敘述

對非集合物件型態的一般欄位應該使用使用isNull()方法判斷是否為空(null)。當JPQL使用以下查詢時:
SELECT DISTINCT e FROM Employee1 e INNER JOIN e.tasks t WHERE t.name IS NULL
對應的Criteria API用法如下:
@Test
public void Criteria_JoinedEntityFieldIsEmpty() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee1.class);
    Root emp1 = sql.from(Employee1.class);
    ListJoin tasks = emp1.join(Employee1_.tasks);
    sql.select(emp1)
        .distinct(true)
        .where(cb.isNull(tasks.get(Task_.name)));
    List resultList = em.createQuery(sql).getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, contains(employee3));
    assertThat(resultList, hasSize(1));
}
行10:
使用CriteriaBuilder的isNull()方法查詢非集合型態的欄位Task_.name是否為空(null)。

在WHERE敘述使用比較(Comparision)關鍵字:MEMBER OF

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

使用CriteriaBuilder的isMember()方法和isNotMember()方法決定關聯的「集合物件」內是否有特定成員。以isMember()為例:
【語法】
<E, C extends Collection<E>>
 Predicate isMember(Expression<E> elem, Expression<C> collection);
泛型E:代表集合物件成員型態。
泛型C:代表集合物件型態。
回傳型態Predicate。
方法參數:
1. Expression<E> elem:型態為E的Expression物件參考,輸入成員物件。
2. Expression<C> collection:型態為C的Expression物件參考,輸入集合物件欄位物件。

使用 MEMBER OF 

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE '777' MEMBER OF e.phoneNumbers
對應的Criteria API用法如下:
@Test
public void Criteria_MemberOf() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  sql.select(emp)
    .where(cb.isMember(cb.literal("777"), emp.get(Employee_.phoneNumbers)));
  TypedQuery typedQuery = em.createQuery(sql);
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  //assertThat(resultList, IsEmptyCollection.empty());
  assertThat(resultList, containsInAnyOrder(employee1, employee3));
  assertThat(resultList, hasSize(2));
}
行8:
查詢欄位Employee_.phoneNumbers內含字串777者。藉由CriteriaBuilder 的literal()方法將String轉換為Expression<String>。

使用 NOT MEMBER OF 

當JPQL使用以下查詢時:
SELECT e FROM Employee e WHERE '777' NOT MEMBER OF e.phoneNumbers
對應的Criteria API用法如下:
@Test 
public void Criteria_NotMemberOf() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  sql.select(emp)
      .where(cb.isNotMember("777", emp.get(Employee_.phoneNumbers)));
  TypedQuery typedQuery = em.createQuery(sql);
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee2, employee4));
  assertThat(resultList, hasSize(2));
}
行8:
使用CriteriaBuilder的isNotMember()方法。第1個參數直接使用String型態,此為Expression<String>的overloading版本。
方法isMember()或isNotMember()的第1個參數也可以是另一個「一般型態」的欄位。當JPQL使用以下查詢時:
SELECT e FROM Employee e 
    WHERE e.primaryPhoneNumber 
            NOT MEMBER OF e.phoneNumbers
對應的Criteria API用法如下,注意行8程式碼:
@Test
public void Criteria_NotMemberOf2() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  sql.select(emp)
      .where(cb.isNotMember(emp.get(Employee_.primaryPhoneNumber),
                            emp.get(Employee_.phoneNumbers)));
  TypedQuery typedQuery = em.createQuery(sql);
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee4, employee2));
  assertThat(resultList, hasSize(2));
}

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

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

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

groupBy() & count()

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

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

行5:

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

groupBy() & avg()

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

MAX()、GROUP BY、HAVING

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

使用聚合(Aggregation)函式

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

JPQL支援在SELECT敘述內使用AVG、COUNT、MAX、MIN、SUM等聚合函式。

COUNT()

當JPQL使用以下查詢時:
SELECT COUNT(e) FROM Employee e
【語法】
Expression<Long> count(Expression<?> x);
1. 回傳型態為Expression<Long>,即Long型態欄位。
2. 方法參數:
    Expression<?> x:不限定欄位型態。
如以下範例行7:
@Test
public void Criteria_Count() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Long.class);
    Root emp = sql.from(Employee.class);
    sql.select(cb.count(emp));
    TypedQuery typedQuery = em.createQuery(sql);
    Long count = typedQuery.getSingleResult();
    System.out.println(count);
    em.close();
    assertEquals(4, count.intValue());
}

AVG()

當JPQL使用以下查詢時:
SELECT AVG(e.salary) FROM Employee e
對應Criteria API可以使用CriteriaBuilder的avg()方法:
【語法】
<N extends Number> Expression<Double> avg(Expression<N> x);
1. 泛型N必須是Number的子類別,即數字型態。
2. 回傳型態為Expression<Double>,因此數字型態取平均後得到Double型態。
3. 方法參數:
    Expression<N> x:需為數字型態的欄位。
如以下範例行7:
@Test
public void Criteria_Average() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Double.class);
    Root emp = sql.from(Employee.class);
    sql.select(cb.avg(emp.get(Employee_.salary)));
    TypedQuery typedQuery = em.createQuery(sql);
    Double average = typedQuery.getSingleResult();
    System.out.println(average);
    em.close();
    assertEquals(2625.0, average, 0.0);
}

max() & greatest()

當JPQL使用以下查詢取得數字欄位的最大數值時:
SELECT MAX(e.salary) FROM Employee e
對應Criteria API可以使用CriteriaBuilder的max()方法: 
【語法】
<N extends Number> Expression<N> max(Expression<N> x);
1. 泛型N必須是Number的子類別,即數字型態。
2. 回傳型態為Expression<N>,即數字型態欄位。
3. 方法參數:
    Expression<N> x:需為數字型態的欄位。
如以下範例行7:
@Test
public void Criteria_Max() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Double.class);
    Root emp = sql.from(Employee.class);
    sql.select( cb.max( emp.get(Employee_.salary) ) );
    TypedQuery typedQuery = em.createQuery(sql);
    Double max = typedQuery.getSingleResult();
    System.out.println(max);
    em.close();
    assertEquals(4000.0, max, 0.0);
}
相似的情況,但若欄位型態非數字但求最大值,如String和Date,則使用CriteriaBuilder的greatest()方法:
【語法】

<X extends Comparable<? super X>> Expression<X> greatest(Expression<X> x);
1. 泛型X必須有實作Comparable介面,因此可比較。
2. 回傳型態為Expression<X>:型態為X的Expression物件參考,代表實作Comparable介面的物件型態的欄位。
3. 方法參數:
    Expression<X> x:型態為X的Expression物件參考,代表實作Comparable介面的物件型態的欄位。
如以下範例行7:
@Test 
public void Criteria_Greatest() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(String.class);
    Root emp = sql.from(Employee.class);
    sql.select( cb.greatest( emp.get(Employee_.name) ) );
    TypedQuery typedQuery = em.createQuery(sql);
    String greatest = typedQuery.getSingleResult();
    System.out.println(greatest);
    em.close();
    assertEquals("Rose", greatest);
}

min() & least()

當JPQL使用以下查詢取得數字欄位的最小數值時:
SELECT MIN(e.salary) FROM Employee e
對應Criteria API可以使用CriteriaBuilder的min()方法:
【語法】

<N extends Number> Expression<N> min(Expression<N> x);
1. 泛型N必須是Number的子類別,即數字型態。
2. 回傳型態為Expression<N>,即數字型態的欄位。
3. 方法參數:
    Expression<N> x:即數字型態的欄位。
如以下範例行7:
@Test
public void Criteria_Min() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Double.class);
    Root emp = sql.from(Employee.class);
    sql.select( cb.min( emp.get(Employee_.salary) ) );
    TypedQuery typedQuery = em.createQuery(sql);
    Double min = typedQuery.getSingleResult();
    System.out.println(min);
    em.close();
    assertEquals(1500.0, min, 0.0);
}
相似的情況,但若欄位型態非數字但求最小值,如String和Date,則使用CriteriaBuilder的least()方法:
【語法】

<X extends Comparable<? super X>> Expression<X> least(Expression<X> x);
1. 泛型X必須有實作Comparable介面,因此可比較。
2. 回傳型態為Expression<X>:型態為X的Expression物件參考,代表實作Comparable介面的物件型態的欄位。
3. 方法參數:
    Expression<X> x:型態為X的Expression物件參考,代表實作Comparable介面的物件型態的欄位。
如以下範例行7:
@Test
public void Criteria_Least() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(String.class);
    Root emp = sql.from(Employee.class);
    sql.select( cb.least( emp.get(Employee_.name) ) );
    TypedQuery typedQuery = em.createQuery(sql);
    String least = typedQuery.getSingleResult();
    System.out.println(least);
    em.close();
    assertEquals("Denise", least);
}

SUM()

當JPQL使用以下查詢時:
SELECT SUM(e.salary) FROM Employee e
對應Criteria API可以使用CriteriaBuilder的sum()方法:
【語法】

<N extends Number> Expression<N> sum(Expression<N> x);
1. 泛型N必須是Number的子類別,即數字型態。
2. 回傳型態為Expression<N>,即為數字型態的欄位。
3. 方法參數:
    Expression<N> x:需為數字型態的欄位。
如以下範例行7:
@Test
public void Criteria_Sum() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Double.class);
    Root emp = sql.from(Employee.class);
    sql.select(cb.sum(emp.get(Employee_.salary)));
    TypedQuery typedQuery = em.createQuery(sql);
    Double sum = typedQuery.getSingleResult();
    System.out.println(sum);
    em.close();
    assertEquals(10500.0, sum.doubleValue(), 0.0);
}

2020年5月31日 星期日

Eclipse與Maven均產生MetaModel程式碼的衝突處理方式

使用Eclipse以下設定:
預設可以產生MetaModel類別的*.java檔案:
但Maven專案在打包程式碼時,預設會在路徑「target/generated-sources/annotations」下同時也產生MetaModel類別的*.java檔案,因此過程出現如下錯誤訊息:
[ERROR] /D:/_myBooks/book4/book-source/c03/jpa-advanced/target/generated-sources/annotations/entity/Staff_.java:[9,17] duplicate class: entity.Staff_
[ERROR] /D:/_myBooks/book4/book-source/c03/jpa-advanced/target/generated-sources/annotations/entity/Employee_.java:[12,17] duplicate class: entity.Employee_    
解決方案之一是:
1. 在pom.xml內加入以下設定,可以將路徑「target/generated-sources/annotations」成為Maven專案認同的程式碼(*.java)來源,一般只認定src和lib:
2. 關閉Eclipse自動產生MetaModel類別的設定,同時將Eclipse產生的MetaModel類別都刪除(若有):
3. 更新(update) Maven專案。
4. 重新打包Maven專案(clean package)。此時MetaModel類別將由Maven產生,並被認可為*.java來源,所以專案打包成功:
5. 過程中適度使用Eclipse的功能refresh和clean專案。

2020年5月23日 星期六

JDBC、JPA、JPQL、CriteriaAPI簡介

JDBC與ORM

我們小時候都有騎自行車的經驗,自行車也幾乎是每個人啟蒙的交通工具。隨著年紀逐漸成長,上學、上班可能需要長程通勤,因此會選擇機車、汽車、鐵路、甚至高鐵等,視需求而定。但不論如何,短程距離還是可能會使用自行車,如共享單車等,因此騎自行車的基本技能還是需要的!
JDBC等同於我們談論「資料保存(persistence)」主題時的自行車。它雖然是基礎,但功能性完全沒問題,存取資料庫不會有JDBC做不到的功能,畢竟JDBC就是Java為存取資料庫而定義的介面。但是隨著需求變得越來越複雜,我們對資料保存的需求也隨之提高,比如說我們需要將Java物件的屬性欄位對應到資料庫的表格欄位,並且為我們建立查詢語句,使我們不需要如使用PreparedStatement時輸入一連串的問號。除此之外我們還需要更複雜的功能:

  1. lazy loading
  2. 隨著物件間的關聯愈來愈多,物件圖譜(object graphs)變得愈加龐大,若關聯物件的屬性欄位的資料都來自資料庫,有時不會希望在由資料庫載入物件時立即載入(eager loading)所有相關資料。比如說系統需要載入PurchaseOrder的集合物件,而每一個物件又包含了LineItem的集合物件;如果只需要PurchaseOrder的其他屬性,事實上一併載入LineItem資料是沒有任何意義的,而且將增加資料庫與JVM的負荷。lazy loading允許僅在需要時才獲取資料。物件圖譜(object graphs)的名詞說明請參閱「Java SE8 OCPJP進階認證指南」一書的「8.4.1了解序列化技術」。
  3. eager fetching
  4. 與lazy loading或eager loading不同,藉由這個機制可以在一個查詢中獲取整個object graphs而不需要以其他SQL反覆來回資料庫與JVM間去加載資料至Java物件中。以前例來說,就是以1個查詢取得PurchaseOrder物件與其關聯的LineItem。
  5. cascading
  6. 有時候對資料庫表格的更改會希望觸發其他連動行為,如刪除PurchaseOrder後,會希望一併刪除關聯的LineItem;或是新增PurchaseOrder時,關聯LineItem一併新增,不需要再特地呼叫EntityManager的persist()或remove()方法。
提供這些進階服務的框架,通稱為object-relational mapping,或簡稱ORM。使用ORM框架開發persistence相關程式可以為開發者減少大量程式碼與時間,也可以將重心從編寫容易出錯的SQL程式碼轉移到商業邏輯需求。
ORM在Java裡已經是正式規格,稱「JPA」。

JPA

Java Persistence API (JPA)扮演Java程式語言和各式資料庫間的橋樑,用於將Java物件對應到關聯式資料庫表格的資料列,因此如同JDBC只定義規格與抽象層,實作則由各供應商負責。JPA的規格讓程式開發人員即便面對不同種類資料庫的資料也能以一致的做法進行新增、讀取、修改、刪除(create/read/update/delete,CRUD)操作,以便相同的程式碼適用於不同的資料庫產品。
JPA不僅可以取代傳統JDBC程式碼處理與資料庫的互動,還可以在適當定義後將資料列的欄位(fields)自動對應(mapping)到domain物件的欄位(fields)以增加便利性,入門觀念可參閱「Java RWD Web企業網站開發指南」一書的「15.3 Object Relational Mapping(ORM)的軟體架構與JPA」。
JPA的組成主要有3個部分:

  1. 實體(Entity)類別
  2. 較舊的JPA版本的實體(Entity)類別需繼承JPA框架提供的類別,導致對框架的依賴性且不容易實施單元測試;較新的JPA版本則使用單純的POJO物件(plain old Java object)作為Entity類別,因此不需要繼承任何框架提供的類別。這類概念風潮由Spring框架帶起,可參閱「Java RWD Web企業網站開發指南」一書的「18.1 Spring 的目的與策略」。Entity類別在執行時期產生的Entity物件則藉由Entity manager和persistent context管理。
  3. ORM (object-relational mapping)架構
  4. Entity類別藉由XML設定檔或annotation標註類別來定義類別欄位與RDBMS資料表欄位的對應關係(ORM, object-relational mapping)的詮釋資料(metadata)。
  5. JPQL (Java Persistence Query Language)查詢語言
  6. JPA除了對不同的資料庫產品建立一致的抽象層,也提供了JPQL以取代各家資料庫紛雜的查詢語言(native query language)。JPQL讓程式開發人員可以對不同種類的資料庫使用一致的查詢語言,並在執行時期依資料庫種類由框架轉換為一般SQL進行資料庫存取。

JPQL

JDBC查詢資料庫使用SQL語言,可以是符合ANSI 92的標準SQL、或是各資料支援的特殊SQL。
藉由Java Persistence Query Language (JPQL)則允許我們以「類似」SQL語法的Java字串存取資料庫。JPQL是建構於各類SQL之上的抽象層,因此可以使用一致的作法存取各種資料庫,具有獨立於各類資料庫的可攜性(portable)特性。

Criteria API

JPA使用JPQL查詢和JDBC使用原生SQL語言查詢相似,都是使用字串來定義;JPA的Criteria API則藉由分析並分拆SQL指令為數個結構,分別以Java物件取代,因此開發上有較大不同。不過,無論是:
  1. 以字串為基礎的JPQL。
  2. 以物件為基礎的Criteria API。
兩者共通性為:
  1. 效能(performance)相似。
  2. 本質上都具備可攜性(portable),不會因為更換底層資料庫而必須修改程式碼。
由於JPQL如同原生SQL以字串方式來定義,因此語法或邏輯的錯誤必須在執行(runtime)時期才能發現;Criteria API的優點是使用Java物件組成,因此若有語法或邏輯上的錯誤可以在編譯(compile)時期提前發現,如同enum都屬於「型態安全(typesafe)」的設計。
另一方面,SQL語言已經脫離Java語言的範疇,不是每個Java程式設計師都能駕輕就熟;若不熟悉SQL語言,改以物件的方式呈現SQL組成,對於他們可能更容易理解並使用。
此外,對於會因為情境不同而需要「動態」組織的SQL指令,使用Criteria API也是一個讓程式更好維護的選項,因為可以減少字串連接的操作以避免bug。
為了讓Criteria API的型態安全的效益可以更發揮,經常會搭配使用「Metamodel API」,這也是特色之一。

Map Join


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

Entity類別以可以關連到Map型態的欄位,如同List或Set,也可以使用@ElementCollection、@ManyToMany、@OneToMany等標註。在JPQL中提供KEY、 VALUE、ENTRY等關鍵字存取Map物件,Criteria API中則為:
  1. Root<Entity>使用join()方法和Map<Key, Value>屬性欄位聯結後,可以取得MapJoin<Entity, Key, Value>物件。
  2. MapJoin<Entity, Key, Value>具備以下方法存取關連到Map型態的欄位:
  • key():取得Map物件的key值,等同JPQL的KEY敘述。
  • value():取得Map物件的value值,等同JPQL的VALUE敘述。
  • entry():同時取得Map物件的key與value值,等同JPQL的ENTRY敘述。
 當JPQL使用以下查詢時:

TypedQuery typedQuery  = em.createQuery(
  "SELECT c.name, KEY(map), VALUE(map) FROM Customer c JOIN c.itemQtyMap map", 
  Tuple.class);
對應的Criteria API用法如下:
@Test
public void Criteria_GetMapKeyValueByTuple() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createTupleQuery();
    Root cust = sql.from(Customer.class);
    MapJoin itemQtyMap = 
                          cust.join(Customer_.itemQtyMap);
    sql.multiselect(cust.get(Customer_.name), 
                    itemQtyMap.key(), 
                    itemQtyMap.value() );
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    List stringList = resultList.stream()
            .map(t -> t.get(0) + ", " + t.get(1) + ", " + t.get(2))
            .collect(Collectors.toList());
    stringList.forEach(System.out::println);
    assertThat(stringList, containsInAnyOrder("jim, computer, 1", 
                                                "jim, mouse, 4", 
                                                "colin, notebook, 3"));
}
  • 行5 
使用Tuple封裝查詢結果的複數欄位。

  • 行7-8

使用Root<Customer> cust的join()方法聯結Map型態的欄位,並回傳MapJoin<Customer, String, Integer>物件。介面MapJoin<1, 2, 3>的泛型指定3種型態:
1. Entity型態,本例為Customer。
2. Map欄位的key型態,本例為String。
3. Map欄位的value型態,本例為Integer。

  • 行9-11

使用CriteriaBuilder物件的multiselect()方法查詢name欄位, Map欄位的key欄位由itemQtyMap.key()表示,Map欄位的value欄位由itemQtyMap.value()表示。
當JPQL使用以下查詢時:
TypedQuery typedQuery = em.createQuery(
    "SELECT ENTRY(map) FROM Customer c JOIN c.itemQtyMap map", 
    Map.Entry.class);  

對應的Criteria API用法如下。本例只查詢Map的key與value等2個欄位,使用Map.Entry型態封裝查詢後的結果:
@Test
public void Criteria_GetEntryByMapEntry() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Map.Entry.class);
    Root cust = sql.from(Customer.class);
    MapJoin orderMap = 
                                  cust.join(Customer_.itemQtyMap);
    sql.select(orderMap.entry());
    TypedQuery typedQuery = em.createQuery(sql);
    // hibernate.version=5.2.10.Final will throw ClassCastException
    List resultList = typedQuery.getResultList();
    List stringList = resultList.stream()
            .map(entry -> entry.getKey()+ ", " + entry.getValue())
            .collect(Collectors.toList());
    stringList.forEach(System.out::println);
    assertThat(stringList, containsInAnyOrder("computer, 1", 
                                                "mouse, 4", 
                                                "notebook, 3"));
}       
  • 行5
使用Map.Entry封裝查詢後的Map的key與value欄位。
  • 行9-11
使用CriteriaBuilder物件的select()方法查詢itemQtyMap.entry(),將同時取得key與value欄位。
值得注意的是,本例在5.2.10.Final的hibernate版本下將拋出ClassCastException的例外,使用較新版本則無此問題。

FETCH JOIN


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

「FETCH」敘述可用於JOIN,如INNER JOIN或LEFT JOIN,可以在一次查詢中獲取全部關聯實體的資料,不需再依個別Entity的關聯而逐筆執行查詢。

INNER JOIN FETCH

當JPQL使用以下語句時:
SELECT DISTINCT e FROM Employee1 e 
                  INNER JOIN FETCH e.tasks t
對應的Criteria API用法如下:
@Test
public void Criteria_InnerJoinFetch() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee1.class);
    Root emp1 = sql.from(Employee1.class);
    Fetch task =  
            emp1.fetch(Employee1_.tasks, JoinType.INNER);
    sql.select(emp1)
        .distinct(true);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    assertThat(resultList, 
            containsInAnyOrder(employee1, employee2, employee3));
    assertThat(resultList, hasSize(3));
    assertEquals(1, keyWordCount(out.getLog(), "Hibernate:"));
}
  • 行7-8
使用Root<Employee1> emp1的fetch()方法並指定:
1. 聯結「欄位」,本例為Employee1_.tasks。
2. 聯結「型態」,本例為JoinType.INNER。
可以得到Fetch<Employee1, Task>物件,和先前Join<Employee1, Task>物件參考相似。本例並無使用該物件參考,實際上並不需要特別宣告物件參考指向方法執行後的回傳結果,只是單純讓讀者了解fetch()方法回傳型態為Fetch物件。
  • 行9
使用sql.select(emp1)看似只查詢Employee1類別,但使用fetch()方法已經一次取回Employee1類別與其關聯的Task類別的所有欄位資料,因此呼叫emp1.getTasks()將可以直接由本地端JVM取得資料,不需要依賴
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Task> tasks;
的宣告逐筆至遠端資料庫中取回資料。優點是可以減少資料庫的shared lock狀況並提升效能,但必須衡量是否有需要這些資料,以及記憶體使用狀況。

LEFT JOIN FETCH

概念與INNER JOIN FETCH相同,只是改用LEFT JOIN。當JPQL使用以下語句時:
SELECT DISTINCT e FROM Employee1 e 
                  LEFT JOIN FETCH e.tasks t
對應的Criteria API用法如下。注意行8使用fetch()方法並指定聯結型態為JoinType.LEFT:
@Test
public void Criteria_LeftJoinFetch() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee1.class);
    Root emp1 = sql.from(Employee1.class);
    Fetch task = 
            emp1.fetch(Employee1_.tasks, JoinType.LEFT);
    sql.select(emp1)
        .distinct(true);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    assertThat(resultList, 
         containsInAnyOrder(employee1, employee2, employee3, employee4));
    assertThat(resultList, hasSize(4));
    assertEquals(1, keyWordCount(out.getLog(), "Hibernate:"));
}

2020年5月15日 星期五

INNER JOIN

使用INNER JOIN且未限定條件

當JPQL使用以下語句時:
SELECT DISTINCT e FROM Employee e 
                  INNER JOIN e.tasks t      
對應的Criteria API用法如下:
public void Criteria_InnnerJoin() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  emp.join(Employee_.tasks);
  sql.select(emp)
      .distinct(true);
  TypedQuery typedQuery = em.createQuery(sql);
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee1, employee2, employee3));
  assertThat(resultList, hasSize(3));
}    
說明
  • 行6:
emp.join(Employee_.tasks); 等價於Employee e INNER JOIN e.tasks t

使用INNER JOIN並限定條件

承前例,但JPQL增加Task欄位值的限定條件:
SELECT DISTINCT e FROM Employee e 
                  INNER JOIN e.tasks t      
                  WHERE t.name='Denise'  
對應的Criteria API用法如下:
@Test
public void Criteria_InnerJoinWhere1() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  Join task = emp.join(Employee_.tasks);
  sql.select(emp)
      .where(cb.equal(task.get(Task_.name), "Denise"))
      .distinct(true);
  TypedQuery typedQuery = em.createQuery(sql);
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee1, employee3));
  assertThat(resultList, hasSize(2));
}
說明:
  • 行7:
因WHERE子句需要INNER JOIN的表格的欄位加以限制條件,因此需要使用變數Join<Employee, Task> task承接Root<Employee>呼叫join()方法後回傳的物件。相似於Root<Employee>,但用Join<Employee, Task>;變數名稱慣例上使用第2個泛型型態Task來命名,本例為「task」。
若需要連續JOIN數個表格,如Entity類別A需要JOIN類別B與C,則依此類推:
Root<A> a = sql.from(A.class);
Join<A, C> c = a.join(A_.b).join(B_.c);
  • 行9:
where(cb.equal(task.get(Task_.name), "Denise"))等價於「WHERE t.name='Denise'」

使用INNER JOIN並指定2表格結合的欄位

承前例,但JPQL條件限定為跨2個表格的欄位必須相等:
SELECT DISTINCT e FROM Employee e 
                  INNER JOIN e.tasks t      
                  WHERE t.name = e.name
對應的Criteria API用法如下:
@Test
public void Criteria_InnerJoinWhere2() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  ListJoin task = emp.join(Employee_.tasks);
  sql.select(emp)
      .where( cb.equal( task.get(Task_.name), emp.get(Employee_.name) ) );
  TypedQuery typedQuery = em.createQuery(sql);
  List resultList = typedQuery.getResultList();
  resultList.forEach(System.out::println);
  em.close();
  assertThat(resultList, containsInAnyOrder(employee2));
  assertThat(resultList, hasSize(1));
}
說明:
  • 行9:
where( cb.equal( task.get(Task_.name), emp.get(Employee_.name) ) );等價於「WHERE t.name = e.name」

使用Constructor敘述封裝複數欄位查詢結果

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

對於不是對Entity查詢單一欄位,或是全部欄位的情況,JPQL和Criteria API預設都是使用Object[]承接複數欄位的查詢結果,也都可以使用non-Entity類別將查詢結果封裝。使用以下JPQL查詢時:
SELECT NEW entity.EmployeeInfo(e.name, e.salary) FROM Employee e
等價於使用以下Criteria API查詢:
@Test
public void Criteria_MultiColumnsWithConstructor1() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(EmployeeInfo.class);
    Root emp = sql.from(Employee.class);
    sql.select( cb.construct(EmployeeInfo.class, 
                       emp.get(Employee_.name), 
                       emp.get(Employee_.salary)) );
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, 
        containsInAnyOrder(myEmp1, myEmp2, myEmp3, myEmp4, myEmp5));
}
說明

  • 行5
CriteriaQuery須指定查詢結果以EmployeeInfo呈現。
  • 行7-9
  1. 使用CriteriaQuery的select()方法。
  2. CriteriaBuilder物件的construct()方法第一個參數指定使用EmployeeInfo為複數欄位的建構標的,後續參數為查詢的複數欄位。
  3. 也可以將行7~行9改用CriteriaQuery的multiselect ()方法:sql.multiselect(emp.get(Employee_.name), emp.get(Employee_.salary));其餘不變,語法更精簡。如方法Criteria_MultiColumnsWithConstructor2()。
  • 行10
TypedQuery也指定查詢結果以EmployeeInfo呈現。

使用TUPLE封裝複數欄位查詢結果

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

使用TUPLE型態是另一種封裝複數查詢欄位的選項。當JPQL使用以下查詢時:
    TypedQuery query = em.createQuery(
            "SELECT e.name, e.salary FROM Employee e", Tuple.class);
對應的Criteria API用法如下。本例中將原本欄位改名,因此由Tuple資料結構取出資料須用別名(alias):
@Test
public void Criteria_Tuple() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createTupleQuery();
    Root emp = sql.from(Employee.class);
    sql.multiselect(
            emp.get(Employee_.name).alias("employeeName"),
            emp.get(Employee_.salary).alias("salary") );
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    // get data by alias 
    for (Tuple tuple : resultList) {
        String employeeName = tuple.get("employeeName", String.class);
        Double salary = tuple.get("salary", Double.class);
        System.out.println(employeeName + "_" + salary);
    }
    em.close();
    // validate result
    List stringList = resultList.stream()
            .map(t -> t.get("employeeName", String.class) + "_" + 
                      t.get("salary", Double.class))
            .collect(Collectors.toList());
    assertThat(stringList,containsInAnyOrder("Jim_3000.0", 
                                            "Rose_4000.0", 
                                            "Denise_1500.0",
                                            "Mike_2000.0"));
}
說明:
  • 行5: 
CriteriaQuery物件指定使用Tuple型態封裝複數欄位的查詢結果。
  • 行8: 
欄位name的別名使用alias()方法設定為"employeeName"。
  • 行9:
欄位salary的別名使用alias()方法設定為"salary"。
  • 行10:
TypedQuery物件同時指定型態為Tuple,後續所有物件若使用泛型者皆同。
  • 行14-15: 
Tuple型態物件使用get()方法取出需指定名稱(別名)和型態:
tuple.get("employeeName", String.class);
tuple.get("salary", Double.class);

2020年5月9日 星期六

WHERE條件值使用繫結變量(Bind Variable)

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

類似JPQL在查詢本體先以「名稱字串」或「位置數字」作為查詢「替代字符」,後續再以setParameter()方法的繫結該「替代字符」與「真實參數值」,如:
1. 使用名稱字串
Query query = em.createQuery(
    "SELECT e FROM Employee e WHERE e.dept = :deptName" );
query.setParameter("deptName", "IT");
2. 使用位置數字
Query query = em.createQuery(
    "SELECT e FROM Employee e WHERE e.dept = ?1" );
query.setParameter(1, "IT");
Criteria API建立ParameterExpression物件並以泛型指定查詢參數型態作為「替代字符」,後續再以setParameter()方法的繫結該「替代字符」與「真實參數值」。繫結方式也分為兩種,分別示範如下。

使用ParameterExpression的物件參考名稱進行繫結

@Test 
public void Criteria_NoNamedParam() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    ParameterExpression param = cb.parameter(String.class);
    Expression expr = cb.equal(emp.get(Employee_.name), param);
    sql.select(emp)
        .where(expr);
    TypedQuery typedQuery = em.createQuery(sql);
    typedQuery.setParameter(param, "Jim");
    Employee p = typedQuery.getSingleResult();
    System.out.println(p);
    assertEquals(e1, p);
    em.close();
}
【說明】

  • 行7
使用CriteriaBuilder的parameter()方法並指定參數型態為String以建立ParameterExpression<String>物件,且物件參考名稱為param。
  • 行8
CriteriaQuery的where ()方法依然需要傳入Expression<Boolean>物件,本例使用CriteriaBuilder的equal()方法建構。
    Expression<Boolean>物件的第二個參數過去使用字面常量,使用繫結變量須改傳入ParameterExpression的物件參考param。
    • 行12
    TypedQuery執行查詢時,使用方法setParameter(param, "Jim")指定前述物件參考param與參數值的繫結關係。

    另外指定ParameterExpression物件的參數名稱進行繫結

    測試方法Criteria_NoNamedParam()和Criteria_NamedParam()程式碼大致相同,除了行7特別指定ParameterExpression物件的參數名稱為myName,並於行12使用該名稱myName進行繫結:
    @Test 
    public void Criteria_NamedParam() {
      // the same codes
      ParameterExpression param = cb.parameter(String.class, “myName”);
      // the same codes
      typedQuery.setParameter("myName", "Jim");
      // the same codes
    }
    
    兩者結果不變。