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