1. 程式人生 > >詳解jpa2.0

詳解jpa2.0

自從 JPA 於 2006 年首次被引入之後,它就得到了 Java 開發社群的廣泛支援。該規範的下一個主要更新 —— 2.0 版本 (JSR 317) —— 將在 2009 年年底完成。JPA 2.0 引入的關鍵特性之一就是 Criteria API,它為 Java 語言帶來了一種獨特的能力:開發一種 Java 編譯器可以在執行時驗證其正確性的查詢。Criteria API 還提供一個能夠在執行時動態地構建查詢的機制。

本文將介紹 Criteria API 和與之密切相關的 元模型(metamodel)概念。您將學習如何使用 Criteria API 開發 Java 編譯器能夠檢查其正確性的查詢,從而減少執行時錯誤,這種查詢優於傳統的基於字串的 Java Persistence Query Language (JPQL) 查詢。藉助使用資料庫函式或匹配模板例項的樣例查詢,我將演示程式設計式查詢構造機制的強大威力,並將其與使用預定義語法的 JPQL 查詢進行對比。本文假設您具備基礎的 Java 語言程式設計知識,並瞭解常見的 JPA 使用,比如 EntityManagerFactory
EntityManager

JPQL 查詢有什麼缺陷?

JPA 1.0 引進了 JPQL,這是一種強大的查詢語言,它在很大程度上導致了 JPA 的流行。不過,基於字串並使用有限語法的 JPQL 存在一些限制。要理解 JPQL 的主要限制之一,請檢視清單 1 中的簡單程式碼片段,它通過執行 JPQL 查詢選擇年齡大於 20 歲的 Person 列表:


清單 1. 一個簡單(並且錯誤)的 JPQL 查詢

EntityManager em = ...;
String jpql = "select p from Person where p.age > 20";
Query query = em.createQuery(jpql);
List result = query.getResultList();

這個基礎的例子顯示了 JPA 1.0 中的查詢執行模型的以下關鍵方面:

  • JPQL 查詢被指定為一個 String(第 2 行)。
  • EntityManager 是構造一個包含給定 JPQL 字串的可執行 查詢例項的工廠(第 3 行)。
  • 查詢執行的結果包含無型別的 java.util.List 的元素。

但是這個簡單的例子有一個驗證的錯誤。該程式碼能夠順利通過編譯,但將在執行時失敗,因為該 JPQL 查詢字串的語法有誤。清單 1 的第 2 行的正確語法為:

String jpql = "select p from Person p where p.age > 20";

不幸的是,Java 編譯器不能發現此類錯誤。在執行時,該錯誤將出現在第 3 或第 4 行(具體行數取決於 JPA 提供者是否在查詢構造或執行期間根據 JPQL 語法解析 JPQL 字串)。

型別安全查詢如何提供幫助?

Criteria API 的最大優勢之一就是禁止構造語法錯誤的查詢。清單 2 使用 CriteriaQuery 介面重新編寫了 清單 1 中的 JPQL 查詢:


清單 2. 編寫 CriteriaQuery 的基本步驟

EntityManager em = ...
QueryBuilder qb = em.getQueryBuilder();
CriteriaQuery< Person> c = qb.createQuery(Person.class);
Root< Person> p = c.from(Person.class);
Predicate condition = qb.gt(p.get(Person_.age), 20);
c.where(condition);
TypedQuery< Person> q = em.createQuery(c); 
List< Person> result = q.getResultList();

清單 2 展示了 Criteria API 的核心構造及其基本使用:

  • 第 1 行通過幾種可用方法之一獲取一個 EntityManager 例項。
  • 在第 2 行,EntityManager 建立 QueryBuilder 的一個例項。QueryBuilderCriteriaQuery 的工廠。
  • 在第 3 行,QueryBuilder 工廠構造一個 CriteriaQuery 例項。CriteriaQuery 被賦予泛型型別。泛型引數宣告 CriteriaQuery 在執行時返回的結果的型別。在構造 CriteriaQuery 時,您可以提供各種結果型別引數 —— 從持久化實體(比如 Person.class)到形式更加靈活的 Object[]
  • 第 4 行在 CriteriaQuery 例項上設定了查詢表示式。查詢表示式是在一個樹中組裝的核心單元或節點,用於指定 CriteriaQuery。圖 1 顯示了在 Criteria API 中定義的查詢表示式的層次結構:
    圖 1. 查詢表示式中的介面層次結構
    查詢表示式中的介面層次結構

    首先,將 CriteriaQuery 設定為 Person.class 查詢。結果返回 Root< Person> 例項 pRoot 是一個查詢表示式,它表示持久化實體的範圍。Root< T> 實際上表示:“對所有型別為 T 的例項計算這個查詢。” 這類似於 JPQL 或 SQL 查詢的 FROM 子句。另外還需要注意,Root< Person> 是泛型的(實際上每個表示式都是泛型的)。型別引數就是表示式要計算的值的型別。因此 Root< Person> 表示一個對 Person.class 進行計算的表示式。第 5 行構造一個 PredicatePredicate 是計算結果為 true 或 false 的常見查詢表示式形式。謂詞由 QueryBuilder 構造,QueryBuilder 不僅是 CriteriaQuery 的工廠,同時也是查詢表示式的工廠。QueryBuilder 包含構造傳統 JPQL 語法支援的所有查詢表示式的 API 方法,並且還包含額外的方法。在 清單 2 中,QueryBuilder 用於構造一個表示式,它將計算第一個表示式引數的值是否大於第二個引數的值。方法簽名為:

  • Predicate gt(Expression< ? extends Number> x, Number y);               
    

    這個方法簽名是展示使用強型別語言(比如 Java)定義能夠檢查正確性並阻止錯誤的 API 的好例子。該方法簽名指定,僅能將值為 Number 的表示式與另一個值也為 Number 的表示式進行比較(例如,不能與值為 String 的表示式進行比較):

    Predicate condition = qb.gt(p.get(Person_.age), 20);
    

    第 5 行有更多學問。注意 qb.gt() 方法的第一個輸入引數:p.get(Person_.age),其中 p 是先前獲得的 Root< Person> 表示式。p.get(Person_.age) 是一個路徑表示式。路徑表示式是通過一個或多個持久化屬性從根表示式進行導航得到的結果。因此,表示式 p.get(Person_.age) 表示使用 Personage 屬性從根表示式 p 導航。您可能不明白 Person_.age 是什麼。您可以將其暫時看作一種表示 Personage 屬性的方法。我將在談論 JPA 2.0 引入的新 Metamodel API 時詳細解釋 Person_.age

    如前所述,每個查詢表示式都是泛型的,以表示表示式計算的值的型別。如果 Person.class 中的 age 屬性被宣告為型別 Integer (或 int),則表示式 p.get(Person_.age) 的計算結果的型別為 Integer。由於 API 中的型別安全繼承,編輯器本身將對無意義的比較丟擲錯誤,比如:

    Predicate condition = qb.gt(p.get(Person_.age, "xyz"));

  • 第 6 行在 CriteriaQuery 上將謂詞設定為其 WHERE 子句。
  • 在第 7 行中,EntityManager 建立一個可執行查詢,其輸入為 CriteriaQuery。這類似於構造一個輸入為 JPQL 字串的可執行查詢。但是由於輸入 CriteriaQuery 包含更多的型別資訊,所以得到的結果是 TypedQuery,它是熟悉的 javax.persistence.Query 的一個擴充套件。如其名所示,TypedQuery 知道執行它返回的結果的型別。它是這樣定義的:

    public interface TypedQuery< T> extends Query {
                 List< T> getResultList();
    }
    

    與對應的無型別超介面相反:

    public interface Query {
    List getResultList();
    }

    很明顯,TypedQuery 結果具有相同的 Person.class 型別,該型別在構造輸入 CriteriaQuery 時由 QueryBuilder 指定(第 3 行)。

  • 在第 8 行中,當最終執行查詢以獲得結果列表時,攜帶的型別資訊展示了其優勢。得到的結果是帶有型別的 Person 列表,從而使開發人員在遍歷生成的元素時省去麻煩的強制型別轉換(同時減少了 ClassCastException 執行時錯誤)。

現在歸納 清單 2 中的簡單例子的基本方面:

  • CriteriaQuery 是一個查詢表示式節點樹。在傳統的基於字串的查詢語言中,這些表示式節點用於指定查詢子句,比如 FROMWHEREORDER BY。圖 2 顯示了與查詢相關的子句:
    圖 2. CriteriaQuery 封裝了傳統查詢的子句
    查詢表示式的介面層次結構
  • 查詢表示式被賦予泛型。一些典型的表示式是:
    • Root< T>,相當於一個 FROM 子句。
    • Predicate,其計算為布林值 true 或 false(事實上,它被宣告為 interface Predicate extends Expression< Boolean>)。
    • Path< T>,表示從 Root< ?> 表示式導航到的持久化屬性。Root< T> 是一個沒有父類的特殊 Path< T>
  • QueryBuilderCriteriaQuery 和各種查詢表示式的工廠。
  • CriteriaQuery 被傳遞給一個可執行查詢並保留型別資訊,這樣可以直接訪問選擇列表的元素,而不需要任何執行時強制型別轉換。

    持久化域的元模型

    討論 清單 2 時指出了一個不常見的構造:Person_.age,它表示 Person 的持久化屬性 age清單 2 使用 Person_.age 形成一個路徑表示式,它通過 p.get(Person_.age)Root< Person> 表示式 p 導航而來。Person_.agePerson_ 類中的公共靜態欄位,Person_靜態、已例項化的規範元模型類,對應於原來的 Person 實體類。

    元模型類描述持久化類的元資料。如果一個類安裝 JPA 2.0 規範精確地描述持久化實體的元資料,那麼該元模型類就是規範的。規範的元模型類是靜態的,因此它的所有成員變數都被宣告為靜態的(也是 public 的)。Person_.age 是靜態成員變數之一。您可以在開發時在原始碼中生成一個具體的 Person_.java例項化 一個規範類。例項化之後,它就可以在編譯期間以強型別的方式引用 Person 的持久化屬性。

    這個 Person_metamodel 類是引用 Person 的元資訊的一種代替方法。這種方法類似於經常使用(有人可能認為是濫用)的 Java Reflection API,但概念上有很大的不同。您可以使用反射獲得關於 java.lang.Class 的例項的元資訊,但是不能以編譯器能夠檢查的方式引用關於 Person.class 的元資訊。例如,使用反射時,您將這樣引用 Person.class 中的 age 欄位:

    Field field = Person.class.getField("age");
    

    不過,這種方法也存在很大的限制,類似於 清單 1 中基於字串的 JPQL 查詢存在的限制。編譯器能夠順利編譯該程式碼,但不能確定它是否可以正常工作。如果該程式碼包含任何錯誤輸入,它在執行時肯定會失敗。反射不能實現 JPA 2.0 的型別安全查詢 API 要實現的功能。

    型別安全查詢 API 必須讓您的程式碼能夠引用 Person 類中的持久化屬性 age,同時讓編譯器能夠在編譯期間檢查錯誤。JPA 2.0 提供的解決辦法通過靜態地公開相同的持久化屬性例項化名為 Person_ 的元模型類(對應於 Person)。

    關於元資訊的討論通常都是令人昏昏欲睡的。所以我將為熟悉的 Plain Old Java Object (POJO) 實體類展示一個具體的元模型類例子(domain.Person),如清單 3 所示:


    清單 3. 一個簡單的持久化實體

    package domain;
    @Entity
    public class Person {
      @Id
      private long ssn;
      private string name;
      private int age;
    
      // public gettter/setter methods
      public String getName() {...}
    }
    

    這是 POJO 的典型定義,並且包含註釋(比如 @Entity@Id ),從而讓 JPA 提供者能夠將這個類的例項作為持久化實體管理。

    清單 4 顯示了 domain.Person 的對應靜態規範元模型類:


    清單 4. 一個簡單實體的規範元模型

    package domain;
    import javax.persistence.metamodel.SingularAttribute;
    
    @javax.persistence.metamodel.StaticMetamodel(domain.Person.class)
    
    public class Person_ {
      public static volatile SingularAttribute< Person,Long> ssn;
      public static volatile SingularAttribute< Person,String> name;
      public static volatile SingularAttribute< Person,Integer> age;
    }
    

    元模型類將原來的 domain.Person 實體的每個持久化屬性宣告為型別為 SingularAttribute< Person,?> 的靜態公共欄位。通過利用這個 Person_ 元模型類,可以在編譯期間引用 domain.Person 的持久化屬性 age — 不是通過 Reflection API,而是直接引用靜態的 Person_.age 欄位。然後,編譯器可以根據 age 屬性宣告的型別實施型別檢查。我已經列舉了一個關於此類限制的例子:QueryBuilder.gt(p.get(Person_.age), "xyz") 將導致編譯器錯誤,因為編譯器通過 QueryBuilder.gt(..) 的簽名和 Person_.age 的型別可以確定 Personage 屬性是一個數字欄位,不能與 String 進行比較。

    其他一些需要注意的要點包括:

    • 元模型 Person_.age 欄位被宣告為型別 javax.persistence.metamodel.SingularAttributeSingularAttribute 是 JPA Metamodel API 中定義的介面之一,我將在下一小節描述它。SingularAttribute< Person, Integer> 的泛型引數表示該類宣告原來的持久化屬性和持久化屬性本身的型別。
    • 元模型類被註釋為 @StaticMetamodel(domain.Person.class) 以將其標記為一個與原來的持久化 domain.Person 實體對應的元模型類。

    Metamodel API

    我將一個元模型類定義為一個持久化實體類的描述。就像 Reflection API 需要其他介面(比如 java.lang.reflect.Fieldjava.lang.reflect.Method )來描述 java.lang.Class 的組成一樣,JPA Metamodel API 也需要其他介面(比如 SingularAttributePluralAttribute)來描述元模型類的型別及其屬性。

    圖 3 顯示了在 Metamodel API 中定義用於描述型別的介面:


    圖 3. Metamodel API 中的持久化型別的介面的層次結構
    圖 3. Metamodel API 中的持久化型別的介面的層次結構

    圖 4 顯示了在 Metamodel API 中定義用於描述屬性的介面:


    圖 4. Metamodel API 中的持久化屬性的介面的層次結構
    圖 4. Metamodel API 中的持久化屬性的介面的層次結構

    JPA 的 Metamodel API 介面比 Java Reflection API 更加專業化。需要更細微的差別來表達關於持久化的豐富元資訊。例如,Java Reflection API 將所有 Java 型別表示為 java.lang.Class。即沒有通過獨立的定義對概念進行區分,比如類、抽象類和介面。當然,您可以詢問 Class 它是一個介面還是一個抽象類,但這與通過兩個獨立的定義表示介面和抽象類的差別不同。

    Java Reflection API 在 Java 語言誕生時就被引入(對於一種常見的多用途程式語言而言,這曾經是一個非常前沿的概念),但是經過多年的發展才認識到強型別系統的用途和強大之處。JPA Metamodel API 將強型別引入到持久化實體中。例如,持久化實體在語義上區分為 MappedSuperClassEntityEmbeddable。在 JPA 2.0 之前,這種語義區分是通過持久化類定義中的對應類級別註釋來表示的。JPA Metamodel 在 javax.persistence.metamodel 包中描述了 3 個獨立的介面( MappedSuperclassTypeEntityTypeEmbeddableType ),以更加鮮明的對比它們的語義特徵。類似地,可以通過介面(比如 SingularAttributeCollectionAttributeMapAttribute)在型別定義級別上區分持久化屬性。

    除了方便描述之外,這些專門化的元模型介面還有實用優勢,能夠幫助構建型別安全的查詢從而減少執行時錯誤。您在前面的例子中看到了一部分優勢,隨著我通過 CriteriaQuery 描述關於連線的例子,您將看到更多優勢。

    執行時作用域

    一般而言,可以將 Java Reflection API 的傳統介面與專門用於描述持久化元資料的 javax.persistence.metamodel 的介面進行比較。要進一步進行類比,則需要對元模型介面使用等效的執行時作用域概念。java.lang.Class 例項的作用域由 java.lang.ClassLoader 在執行時劃分。一組相互引用的 Java 類例項必須在 ClassLoader 作用域下定義。作用域的邊界是嚴格封閉 的,如果在 ClassLoader L 作用域下定義的類 A 試圖引用不在 ClassLoader L 作用域之內的類 B,結果將收到可怕的 ClassNotFoundExceptionNoClassDef FoundError(對於處理包含多個 ClassLoader 的環境的開發人員或部署人員而言,問題就複雜了)。

    現在將一組嚴格的可相互引用的類稱為執行時作用域,而在 JPA 1.0 中稱為持久化單元。持久化單元作用域的持久化實體在 META-INF/persistence.xml 檔案的 < class> 子句中列舉。在 JPA 2.0 中,通過 javax.persistence.metamodel.Metamodel 介面讓開發人員可以在執行時使用作用域。Metamodel 介面是特定持久化單元知道的所有持久化實體的容器,如圖 5 所示:


    圖 5. 元模型介面是持久化單元中的型別的容器
    圖 5. 元模型介面是持久化單元中的型別的容器

    這個介面允許通過元模型元素的對應持久化實體類訪問元模型元素。例如,要獲得對 Person 持久化實體的持久化元資料的引用,可以編寫:

    EntityManagerFactory emf = ...;
    Metamodel metamodel = emf.getMetamodel();
    EntityType< Person> pClass = metamodel.entity(Person.class);
    

    這是一個用類的名稱通過 ClassLoader 獲得 Class 的類比:

    ClassLoader classloader =  Thread.currentThread().getContextClassLoader();
    Class< ?> clazz = classloader.loadClass("domain.Person");
    

    可以在執行時瀏覽 EntityType< Person> 獲得在 Person 實體中宣告的持久化屬性。如果應用程式在 pClass(比如 pClass.getSingularAttribute("age", Integer.class))上呼叫一個方法,它將返回一個 SingularAttribute< Person, Integer> 例項,該例項與例項化規範元模型類的靜態 Person_.age 成員相同。最重要的是,對於應用程式可以通過 Metamodel API 在執行時引用的屬性,是通過例項化靜態規範元模型 Person_ 類向 Java 編譯器提供的。

    除了將持久化實體分解為對應的元模型元素之外,Metamodel API 還允許訪問所有已知的元模型類 (Metamodel.getManagedTypes()),或者通過類的持久化資訊訪問元模型類,例如 embeddable(Address.class),它將返回一個 EmbeddableType< Address> 例項(ManagedType< > 的子介面)。

    在 JPA 中,關於 POJO 的元資訊使用帶有原始碼註釋(或 XML 描述符)的持久化元資訊進一步進行區分 —— 比如類是否是嵌入的,或者哪個欄位用作主鍵。持久化元資訊分為兩大類:持久化(比如 @Entity)和對映(比如 @Table)。在 JPA 2.0 中,元模型僅為持久化註釋(不是對映註釋)捕捉元資料。因此,使用當前版本的 Metamodel API 可以知道哪些欄位是持久化的,但不能找到它們對映到的資料庫列。

    規範和非規範

    儘管 JPA 2.0 規範規定了規範的靜態元模型類的精確樣式(包括元模型類的完整限定名及其靜態欄位的名稱),應用程式也能夠編寫這些元模型類。如果應用程式開發人員編寫元模型類,這些類就稱為非規範元模型。現在,關於非規範元模型的規範還不是很詳細,因此對非規範元模型的支援不能在 JPA 提供者之間移植。您可能已經注意到,公共靜態欄位僅在規範元模型中宣告,而沒有初始化。宣告之後就可以在開發 CriteriaQuery 時引用這些欄位。但是,必須在執行時給它們賦值才有意義。儘管為規範元模型的欄位賦值是 JPA 提供者的責任,但非規範元模型則不存在這一要求。使用非規範元模型的應用程式必須依賴於特定供應商機制,或開發自己的機制來在執行時初始化元模型屬性的欄位值。

    註釋處理和元模型生成

    如果您有許多持久化實體,您將傾向於不親自編寫元模型類,這是很自然的事情。持久化提供者應該 為您生成這些元模型類。在規範中沒有強制規定這種工具或生成機制,但是 JPA 之間已經私下達成共識,他們將使用在 Java 6 編譯器中整合的 Annotation Processor 工具生成規範元模型。Apache OpenJPA 提供一個工具來生成這些元模型類,其生成方式有兩種,一是在您為持久化實體編譯原始碼時隱式地生成,二是通過顯式地呼叫指令碼生成。在 Java 6 以前,有一個被廣泛使用的稱為 apt 的 Annotation Processor 工具,但在 Java 6 中,編譯器和 Annotation Processor 的合併被定義為標準的一部分。

    要像持久化提供者一樣在 OpenJPA 中生成這些元模型類,僅需在編譯器的類路徑中使用 OpenJPA 類庫編譯 POJO 實體:

    $ javac domain/Person.java
    

    將生成規範元模型 Person_ 類,它將位於 Person.java 所在的目錄,並且作為該編譯的一部分。

    編寫型別安全的查詢

    到目前為止,我已經構建了 CriteriaQuery 的元件和相關的元模型類。現在,我將展示如何使用 Criteria API 開發一些查詢。

    函式表示式

    函式表示式將一個函式應用到一個或多個輸入引數以建立新的表示式。函式表示式的型別取決於函式的性質及其引數的型別。輸入引數本身可以是表示式或文字值。編譯器的型別檢查規則與 API 簽名結合確定什麼是合法輸入。

    考慮一個對輸入表示式應用平均值的單引數表示式。CriteriaQuery 選擇所有 Account 的平均餘額,如清單 5 所示:


    清單 5. CriteriaQuery 中的函式表示式

    CriteriaQuery< Double> c = cb.createQuery(Double.class);
    Root< Account> a = c.from(Account.class);
    
    c.select(cb.avg(a.get(Account_.balance)));
    

    等效的 JPQL 查詢為:

    String jpql = "select avg(a.balance) from Account a";
    

    清單 5 中,QueryBuilder 工廠(由變數 cb 表示)建立一個 avg() 表示式,並將其用於查詢的 select() 子句。

    該查詢表示式是一個構建塊,可以通過組裝它為查詢定義最後的選擇謂詞。清單 6 中的例子顯示了通過導航到 Account 的餘額建立的 Path 表示式,然後 Path 表示式被用作兩個二進位制函式表示式( greaterThan()lessThan())的輸入表示式,這兩個表示式的結果都是一個布林表示式或一個謂詞。然後,通過 and() 操作合併謂詞以形成最終的選擇謂詞,查詢的 where() 子句將計算該謂詞:


    清單 6. CriteriaQuery 中的 where() 謂詞

    CriteriaQuery< Account> c = cb.createQuery(Account.class);
    Root< Account> account = c.from(Account.class);
    Path< Integer> balance = account.get(Account_.balance);
    c.where(cb.and
           (cb.greaterThan(balance, 100), 
            cb.lessThan(balance), 200)));
    

    等效的 JPQL 查詢為:

    "select a from Account a where a.balance>100 and a.balance< 200";
    

    符合謂詞

    某些表示式(比如 in())可以應用到多個表示式。清單 7 給出了一個例子:


    清單 7. CriteriaQuery 中的多值表示式

    CriteriaQuery< Account> c = cb.createQuery(Account.class);
    Root< Account> account = c.from(Account.class);
    Path< Person> owner = account.get(Account_.owner);
    Path< String> name = owner.get(Person_.name);
    c.where(cb.in(name).value("X").value("Y").value("Z"));
    

    這個例子通過兩個步驟從 Account 進行導航,建立一個表示帳戶所有者的名稱的路徑。然後,它建立一個使用路徑表示式作為輸入的 in() 表示式。in() 表示式計算它的輸入表示式是否等於它的引數之一。這些引數通過 value() 方法在 In< T> 表示式上指定,In< T> 的簽名如下所示:

    In< T> value(T value); 
    

    注意如何使用 Java 泛型指定僅對值的型別為 T 的成員計算 In< T> 表示式。因為表示 Account 所有者的名稱的路徑表示式的型別為 String,所以與值為 String 型別的引數進行比較才有效,String 值引數可以是字面量或計算結果為 String 的另一個表示式。

    清單 7 中的查詢與等效(正確)的 JPQL 進行比較:

    "select a from Account a where a.owner.name in ('X','Y','Z')";
    

    在 JPQL 中的輕微疏忽不僅不會被編輯器檢查到,它還可能導致意外結果。例如:

    "select a from Account a where a.owner.name in (X, Y, Z)";
    

    連線關係

    儘管 清單 6清單 7 中的例子將表示式用作構建塊,查詢都是基於一個實體及其屬性之上的。但是查詢通常涉及到多個實體,這就要求您將多個實體連線 起來。CriteriaQuery 通過型別連線表示式 連線兩個實體。型別連線表示式有兩個型別引數:連線源的型別和連線目標屬性的可繫結型別。例如,如果您想查詢有一個或多個 PurchaseOrder 沒有發出的 Customer,則需要通過一個表示式將 Customer 連線到 PurchaseOrder,其中 Customer 有一個名為 orders 型別為 java.util.Set< PurchaseOrder> 的持久化屬性,如清單 8 所示:


    清單 8. 連線多值屬性

    CriteriaQuery< Customer> q = cb.createQuery(Customer.class);
    Root< Customer> c = q.from(Customer.class);
    SetJoin< Customer, PurchaseOrder> o = c.join(Customer_.orders);
    

    連線表示式從根表示式 c 建立,持久化屬性 Customer.orders 由連線源(Customer)和 Customer.orders 屬性的可繫結型別進行引數化,可繫結型別是 PurchaseOrder不是 已宣告的型別 java.util.Set< PurchaseOrder>。此外還要注意,因為初始屬性的型別為 java.util.Set,所以生成的連線表示式為 SetJoin,它是專門針對型別被宣告為 java.util.Set 的屬性的 Join。類似地,對於其他受支援的多值持久化屬性型別,該 API 定義 CollectionJoinListJoinMapJoin。(圖 1 顯示了各種連線表示式)。在 清單 8 的第 3 行不需要進行顯式的轉換,因為 CriteriaQuery 和 Metamodel API 通過覆蓋 join() 的方法能夠識別和區分宣告為 java.util.CollectionList 或者 SetMap 的屬性型別。

    在查詢中使用連線在連線實體上形成一個謂詞。因此,如果您想要選擇有一個或多個未傳送 PurchaseOrderCustomer,可以通過狀態屬性從連線表示式 o 進行導航,然後將其與 DELIVERED 狀態比較,並否定謂詞:

    Predicate p = cb.equal(o.get(PurchaseOrder_.status), Status.DELIVERED)
            .negate();
    

    建立連線表示式需要注意的一個地方是,每次連線一個表示式時,都會返回一個新的表示式,如清單 9 所示:


    清單 9. 每次連線建立一個唯一的例項

    SetJoin< Customer, PurchaseOrder> o1 = c.join(Customer_.orders);
    SetJoin< Customer, PurchaseOrder> o2 = c.join(Customer_.orders);
    assert o1 == o2;
    

    清單 9 中對兩個來自相同表示式 c 的連線表示式的等同性斷言將失敗。因此,如果查詢的謂詞涉及到未傳送並且值大於 $200 的 PurchaseOrder,那麼正確的構造是將 PurchaseOrder 與根 Customer 表示式連線起來(僅一次),把生成的連線表示式分配給本地變數(等效於 JPQL 中的範圍變數),並在構成謂詞時使用本地變數。

    使用引數

    回顧一下本文初始的 JPQL 查詢(正確那個):

    String jpql = "select p from Person p where p.age > 20";
    

    儘管編寫查詢時通常包含常量文字值,但這不是一個良好實踐。良好實踐是引數化查詢,從而僅解析或準備查詢一次,然後再快取並重用它。因此,編寫查詢的最好方法是使用命名引數:

    String jpql = "select p from Person p where p.age > :age";
    

    引數化查詢在查詢執行之前繫結引數的值:

    Query query = em.createQuery(jpql).setParameter("age", 20);
    List result = query.getResultList();
    

    在 JPQL 查詢中,查詢字串中的引數以命名方式(前面帶有冒號,例如 :age)或位置方式(前面帶有問號,例如 ?3)編碼。在 CriteriaQuery 中,引數本身就是查詢表示式。與其他表示式一樣,它們是強型別的,並且由表示式工廠(即 QueryBuilder)構造。然後,可以引數化 清單 2 中的查詢,如清單 10 所示:


    清單 10. 在 CriteriaQuery 中使用引數

    ParameterExpression< Integer> age = qb.parameter(Integer.class);
    Predicate condition = qb.gt(p.get(Person_.age), age);
    c.where(condition);
    TypedQuery< Person> q = em.createQuery(c); 
    List< Person> result = q.setParameter(age, 20).getResultList();
    

    比較該引數使用和 JPQL 中的引數使用:引數表示式被建立為帶有顯式型別資訊 Integer,並且被直接用於將值 20 繫結到可執行查詢。額外的型別資訊對減少執行時錯誤十分有用,因為阻止引數與包含不相容型別的表示式比較,或阻止引數與不相容型別的值繫結。JPQL 查詢的引數不能提供任何編譯時安全。

    清單 10 中的例子顯示了一個直接用於繫結的未命名錶達式。還可以在構造引數期間為引數分配第二個名稱。對於這種情況,您可以使用這個名稱將引數值繫結到查詢。不過,您不可以使用位置引數。線性 JPQL 查詢字串中的整數位置有一定的意義,但是不能在概念模型為查詢表示式樹的 CriteriaQuery 上下文中使用整數位置。

    JPA 查詢引數的另一個有趣方面是它們沒有內部值。值繫結到可執行查詢上下文中的引數。因此,可以合法地從相同的 CriteriaQuery 建立兩個獨立可執行的查詢,併為這些可執行查詢的相同引數繫結兩個整數值。

    預測結果

    您已經看到 CriteriaQuery 在執行時返回的結果已經在 QueryBuilder 構造 CriteriaQuery 時指定。查詢的結果被指定為一個或多個預測條件。可以通過兩種方式之一在 CriteriaQuery 介面上指定預測條件:

    CriteriaQuery< T> select(Selection< ? extends T> selection);
    CriteriaQuery< T> multiselect(Selection< ?>... selections);
    

    最簡單並且最常用的預測條件是查詢候選類。它可以是隱式的,如清單 11 所示:


    清單 11. CriteriaQuery 預設選擇的候選區段

    CriteriaQuery< Account> q = cb.createQuery(Account.class);
    Root< Account> account = q.from(Account.class);
    List< Account> accounts = em.createQuery(q).getResultList();
    

    清單 11 中,來自 Account 的查詢沒有顯式地指定它的選擇條件,並且和顯式地選擇的候選類一樣。清單 12 顯示了一個使用顯式選擇條件的查詢:


    清單 12. 使用單個顯式選擇條件的 CriteriaQuery

    CriteriaQuery< Account> q = cb.createQuery(Account.class);
    Root< Account> account = q.from(Account.class);
    q.select(account);
    List< Account> accounts = em.createQuery(q).getResultList();
    

    如果查詢的預測結果不是候選持久化實體本身,那麼可以通過其他幾個構造方法來生成查詢的結果。這些構造方法包含在 QueryBuilder 介面中,如清單 13 所示:


    清單 13. 生成查詢結果的方法

    < Y> CompoundSelection< Y> construct(Class< Y> result, Selection< ?>... terms);
        CompoundSelection< Object[]> array(Selection< ?>... terms);
        CompoundSelection< Tuple> tuple(Selection< ?>... terms);
    

    清單 13 中的方法構建了一個由其他幾個可選擇的表示式組成的預測條件。construct() 方法建立給定類引數的一個例項,並使用來自輸入選擇條件的值呼叫一個建構函式。例如,如果 CustomerDetails — 一個非持久化實體 — 有一個接受 Stringint 引數的構造方法,那麼 CriteriaQuery 可以通過從選擇的 Customer — 一個持久化實體 — 例項的名稱和年齡建立例項,從而返回 CustomerDetails 作為它的結果,如清單 14 所示:


    清單 14. 通過 construct() 將查詢結果包放入類的例項

    CriteriaQuery< CustomerDetails> q = cb.createQuery(CustomerDetails.class);
    Root< Customer> c = q.from(Customer.class);
    q.select(cb.construct(CustomerDetails.class,
                  c.get(Customer_.name), c.get(Customer_.age));
    

    可以將多個預測條件合併在一起,以組成一個表示 Object[]Tuple 的複合條件。清單 15 顯示瞭如何將結果包裝到 Object[] 中:


    清單 15. 將結果包裝到 Object[]

    CriteriaQuery< Object[]> q = cb.createQuery(Object[].class);
    Root< Customer> c = q.from(Customer.class);
    q.select(cb.array(c.get(Customer_.name), c.get(Customer_.age));
    List< Object[]> result = em.createQuery(q).getResultList();
    

    這個查詢返回一個結果列表,它的每個元素都是一個長度為 2 的 Object[],第 0 個數組元素為 Customer 的名稱,第 1 個數組元素為 Customer 的年齡。

    Tuple 是一個表示一行資料的 JPA 定義介面。從概念上看,Tuple 是一個 TupleElement 列表 — 其中 TupleElement 是源自單元和所有查詢表示式的根。包含在 Tuple 中的值可以被基於 0 的整數索引訪問(類似於熟悉的 JDBC 結果),也可以被 TupleElement 的別名訪問,或直接通過 TupleElement 訪問。清單 16 顯示瞭如何將結果包裝到 Tuple 中:


    清單 16. 將查詢結果包裝到 Tuple

    CriteriaQuery< Tuple> q = cb.createTupleQuery();
    Root< Customer> c = q.from(Customer.class);
    TupleElement< String> tname = c.get(Customer_.name).alias("name");
    q.select(cb.tuple(tname, c.get(Customer_.age).alias("age");
    List< Tuple> result = em.createQuery(q).getResultList();
    String name = result.get(0).get(name);
    String age  = result.get(0).get(1)