程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> JPA 2.0中的動態類型安全查詢

JPA 2.0中的動態類型安全查詢

編輯:關於JAVA

自從 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 的一個實例。QueryBuilder 是 CriteriaQuery 的工廠。

在第 3 行,QueryBuilder 工廠構造一個 CriteriaQuery 實例。CriteriaQuery 被賦予泛型類型。泛型參數聲明 CriteriaQuery 在執行時返回的結果的類型。在構造 CriteriaQuery 時,您可以提供各種結果類型參數 —— 從持久化實體(比如 Person.class)到形式更加靈活的 Object[]。

第 4 行在 CriteriaQuery 實例上設置了查詢表達式。查詢表達式是在一個樹中組裝的核心單元或節點,用於指定 CriteriaQuery。圖 1 顯示了在 Criteria API 中定義的查詢表達式的層次結構:

圖 1. 查詢表達式中的接口層次結構

首先,將 CriteriaQuery 設置為從 Person.class 查詢。結果返回 Root<Person> 實例 p。Root 是一個查詢表達式,它表示持久化實體的范圍。Root<T> 實際上表示:“對所有類型為 T 的實例計算這個查詢。” 這類似於 JPQL 或 SQL 查詢的 FROM 子句。另外還需要注意,Root<Person> 是泛型的(實際上每個表達式都是泛型的)。類型參數就是表達式要計算的值的類型。因此 Root<Person> 表示一個對 Person.class 進行計算的表達式。

第 5 行構造一個 Predicate。Predicate 是計算結果為 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) 表示使用 Person 的 age 屬性從根表達式 p 導航。您可能不明白 Person_.age 是什麼。您可以將其暫時看作一種表示 Person 的 age 屬性的方法。我將在談論 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 是一個查詢表達式節點樹。在傳統的基於字符串的查詢語言中,這些表達式節點用於指定查詢子句,比如 FROM、WHERE 和 ORDER BY。圖 2 顯示了與查詢相關的子句:

圖 2. CriteriaQuery 封裝了傳統查詢的子句

查詢表達式被賦予泛型。一些典型的表達式是:

Root<T>,相當於一個 FROM 子句。

Predicate,其計算為布爾值 true 或 false(事實上,它被聲明為 interface Predicate extends Expression<Boolean>)。

Path<T>,表示從 Root<?> 表達式導航到的持久化屬性。Root<T> 是一個沒有父類的特殊 Path<T>。

QueryBuilder 是 CriteriaQuery 和各種查詢表達式的工廠。

CriteriaQuery 被傳遞給一個可執行查詢並保留類型信息,這樣可以直接訪問選擇列表的元素,而不需要任何運行時強制類型轉換。

持久化域的元模型

討論 清單 2 時指出了一個不常見的構造:Person_.age,它表示 Person 的持久化屬性 age。清單 2 使用 Person_.age 形成一個路徑表達式,它通過 p.get(Person_.age) 從 Root<Person> 表達式 p 導航而來。Person_.age 是 Person_ 類中的公共靜態字段,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 的類型可以確定 Person 的 age 屬性是一個數字字段,不能與 String 進行比較。

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

元模型 Person_.age 字段被聲明為類型 javax.persistence.metamodel.SingularAttribute。SingularAttribute 是 JPA Metamodel API 中定義的接口之一,我將在下一小節描述它。SingularAttribute<Person, Integer> 的泛型參數表示該類聲明原來的持久化屬性和持久化屬性本身的類型。

元模型類被注釋為 @StaticMetamodel(domain.Person.class) 以將其標記為一個與原來的持久化 domain.Person 實體對應的元模型類。

Metamodel API

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

圖 3 顯示了在 Metamodel API 中定義用於描述類型的接口:

圖 3. 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 將強類型引入到持久化實體中。例如,持久化實體在語義上區分為 MappedSuperClass、Entity 和 Embeddable。在 JPA 2.0 之前,這種語義區分是通過持久化類定義中的對應類級別注釋來表示的。JPA Metamodel 在 javax.persistence.metamodel 包中描述了 3 個獨立的接口( MappedSuperclassType、EntityType 和 EmbeddableType ),以更加鮮明的對比它們的語義特征。類似地,可以通過接口(比如 SingularAttribute、CollectionAttribute 和 MapAttribute)在類型定義級別上區分持久化屬性。

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

運行時作用域

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

現在將一組嚴格的可相互引用的類稱為運行時作用域,而在 JPA 1.0 中稱為持久化單元。持久化單元作用域的持久化實體在 META-INF/persistence.xml 文件的 <class> 子句中枚舉。在 JPA 2.0 中,通過 javax.persistence.metamodel.Metamodel 接口讓開發人員可以在運行時使用作用域。Metamodel 接口是特定持久化單元知道的所有持久化實體的容器,如圖 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 提供者的責任,但非規范元模型則不存在這一要求。使用非規范元模型的應用程序必須依賴於特定供應商機制,或開發自己的機制來在運行時初始化元模型屬性的字段值。

代碼生成和可用性

自動生成源代碼通常讓人擔心。為規范元模型生成源代碼更是如此。生成的類在開發期間使用,構建 CriteriaQuery 的其他代碼部分直接在運行時引用它們,從而留下一些可用性問題:

生成的源代碼文件應該在初始源代碼所在的目錄中,還是在一個獨立的或與輸出目錄相關的目錄中?

是否應該在版本控制配置管理系統中檢查源代碼文件?

如何維護初始 Person 實體定義及其規范 Person_ 元模型之間的對應關系?例如,如果要編輯 Person.java 以添加其他持久化屬性,或通過重構重命名持久化屬性,那麼應該怎麼辦呢?

在撰寫本文時,這些問題還沒有確切的答案。

注釋處理和元模型生成

如果您有許多持久化實體,您將傾向於不親自編寫元模型類,這是很自然的事情。持久化提供者應該 為您生成這些元模型類。在規范中沒有強制規定這種工具或生成機制,但是 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() 子句。

Fluent API

如這個例子所示,Criteria API 方法經常返回可以直接在相關方法中使用的類型,從而提供了一種稱為 Fluent API 的流行編程風格。

該查詢表達式是一個構建塊,可以通過組裝它為查詢定義最後的選擇謂詞。清單 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 定義 CollectionJoin、ListJoin 和 MapJoin。(圖 1 顯示了各種連接表達式)。在 清單 8 的第 3 行不需要進行顯式的轉換,因為 CriteriaQuery 和 Metamodel API 通過覆蓋 join() 的方法能夠識別和區分聲明為 java.util.Collection 或 List 或者 Set 或 Map 的屬性類型。

在查詢中使用連接在連接實體上形成一個謂詞。因此,如果您想要選擇有一個或多個未發送 PurchaseOrder 的 Customer,可以通過狀態屬性從連接表達式 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 — 一個非持久化實體 — 有一個接受 String 和 int 參數的構造方法,那麼 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);

嵌套限制

從理論上講,可以通過嵌套 Tuple 等條件(它的元素本身為 Object[] 或 Tuple)來構成復雜的結果。不過,JPA 2.0 規范禁止此類嵌套。multiselect() 的輸入條件不能是數組或值為二元組的復合條件。允許作為 multiselect() 參數的唯一復合條件由 construct() 方法創建(該方法僅表示一個元素)。

不過,OpenJPA 沒有限制在一個復合選擇條件中嵌套其他的復合選擇條件。

這個查詢返回一個結果列表,它的每個元素都是一個 Tuple。反過來,每個二元組都帶有兩個元素 — 可以被每個 TupleElement 的索引或別名(如果有的話)訪問,或直接被 TupleElement 訪問。清單 16 中需要注意的兩點是 alias() 的使用,它是將一個名稱綁定到查詢表達式的一種方式(創建一個新的副本),和 QueryBuilder 上的 createTupleQuery() 方法,它僅是 createQuery(Tuple.class) 的代替物。

這些能夠改變結果的方法的行為和在構造期間被指定為 CriteriaQuery 的類型參數結果共同組成 multiselect() 方法的語義。這個方法根據最終實現結果的 CriteriaQuery 的結果類型解釋它的輸入條件。要像 清單 14 一樣使用 multiselect() 構造 CustomerDetails 實例,您需要將 CriteriaQuery 的類型指定為 CustomerDetails,然後使用將組成 CustomerDetails 構造方法的條件調用 multiselect(),如清單 17 所示:

清單 17. 基於結果類型的 multiselect() 解釋條件

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

因為查詢結果類型為 CustomerDetails,multiselect() 將其預測條件解釋為 CustomerDetails 構造方法參數。如將查詢指定為返回 Tuple,那麼帶有相同參數的 multiselect() 方法將創建 Tuple 實例,如清單 18 所示:

清單 18. 使用 multiselect() 方法創建 Tuple 實例

CriteriaQuery<Tuple> q = cb.createTupleQuery();
Root<Customer> c = q.from(Customer.class);
q.multiselect(c.get(Customer_.name), c.get(Customer_.age));

如果以 Object 作為結果類型或沒有指定類型參數時,multiselect() 的行為會變得更加有趣。在這些情況中,如果 multiselect() 使用單個輸入條件,那麼返回值將為所選擇的條件。但是如果 multiselect() 包含多個輸入條件,結果將得到一個 Object[]。

高級特性

到目前為止,我主要強調了 Criteria API 的強類型,以及它如何幫助減少出現在基於字符串 JPQL 查詢中的語義錯誤。Criteria API 還是以編程的方式構建查詢的機制,因此通常被稱為動態 查詢 API。編程式查詢構造 API 的威力是無窮的,但它的利用還取決於用戶的創造能力。我將展示 4 個例子:

使用弱類型的 API 構建動態查詢

使用數據庫支持的函數作為查詢表達式來擴展語法

編輯查詢實現 “在結果中搜索” 功能

根據例子進行查詢 — 數據庫社區熟悉的模式

弱類型和動態查詢構建

Criteria API 的強類型檢查基於開放期間的實例化元模型類的可用性。不過,在某些情況下,選擇的實體僅能夠在運行時決定。為了支持這種用法,Criteria API 方法提供一個並列版本,其中持久化屬性通過它們的名稱進行引用(類似於 Java Reflection API),而不是引用實例化靜態元模型屬性。該 API 的這個並列版本可以通過犧牲編譯時類型檢查來真正地支持動態查詢構造。清單 19 使用弱類型 API 重新編寫了 清單 6 中的代碼:

清單 19. 弱類型查詢

Class<Account> cls =Class.forName("domain.Account");
Metamodel model = em.getMetamodel();
EntityType<Account> entity = model.entity(cls);
CriteriaQuery<Account> c = cb.createQuery(cls);
Root<Account> account = c.from(entity);
Path<Integer> balance = account.<Integer>get("balance");
c.where(cb.and
     (cb.greaterThan(balance, 100),
     cb.lessThan(balance), 200)));

不過,弱類型 API 不能夠返回正確的泛型表達式,因此生成一個編輯器來警告未檢查的轉換。一種消除這些煩人的警告消息的方法是使用 Java 泛型不常用的工具:參數化方法調用,比如 清單 19 中通過調用 get() 方法獲取路徑表達式。

可擴展數據庫表達式

動態查詢構造機制的獨特優勢是它的語法是可擴展的。例如,您可以在 QueryBuilder 接口中使用 function() 方法創建數據庫支持的表達式:

<T> Expression<T> function(String name, Class<T> type, Expression<?>...args);

function() 方法創建一個帶有給定名稱和 0 個或多個輸入表達式的表達式。function() 表達式的計算結果為給定的類型。這允許應用程序創建一個計算數據庫的查詢。例如,MySQL 數據庫支持 CURRENT_USER() 函數,它為服務器用於驗證當前客戶機的 MySQL 帳戶返回一個由用戶名和主機名組成的 UTF-8 字符串。應用程序可以在 CriteriaQuery 中使用未帶參數的 CURRENT_USER() 函數,如清單 20 所示:

清單 20. 在 CriteriaQuery 中使用特定於數據庫的函數

CriteriaQuery<Tuple> q = cb.createTupleQuery();
Root<Customer> c = q.from(Customer.class);
Expression<String> currentUser =
   cb.function("CURRENT_USER", String.class, (Expression<?>[])null);
q.multiselect(currentUser, c.get(Customer_.balanceOwed));

注意,在 JPQL 中不能表達等效的查詢,因為它的語法僅支持固定數量的表達式。動態 API 不受固定數量表達式的嚴格限制。

可編輯查詢

可以以編程的方式編輯 CriteriaQuery。可以改變查詢的子句,比如它的選擇條件、WHERE 子句中的選擇謂詞和 ORDER BY 子句中的排序條件。可以在典型的 “在結果中搜索” 工具中使用這個編輯功能,以添加更多限制在後續步驟中進一步細化查詢謂詞。

清單 21 中的例子創建了一個根據名稱對結果進行排序的查詢,然後編輯該查詢以根據郵政編碼進行查詢:

清單 21. 編輯 CriteriaQuery

CriteriaQuery<Person> c = cb.createQuery(Person.class);
Root<Person> p = c.from(Person.class);
c.orderBy(cb.asc(p.get(Person_.name)));
List<Person> result = em.createQuery(c).getResultList();
// start editing
List<Order> orders = c.getOrderList();
List<Order> newOrders = new ArrayList<Order>(orders);
newOrders.add(cb.desc(p.get(Person_.zipcode)));
c.orderBy(newOrders);
List<Person> result2 = em.createQuery(c).getResultList();

OpenJPA 中的常駐計算

通過使用 OpenJPA 的擴展特性,清單 21 中的 “在結果中搜索” 例子可以在內存中 計算編輯查詢,從而獲得更高的效率。這個例子表明編輯查詢的結果是原始結果的嚴格子集。因為 OpenJPA 能夠在指定候選集合時從內存計算查詢,所以僅需修改 清單 21 的最後一行代碼就可以提供原始查詢的結果:

List<Person> result2 =
  em.createQuery(c).setCandidateCollection(result).getResultList();

在 CriteriaQuery 上的 setter 方法 — select()、where() 或 orderBy() — 使用新的參數替換先前的值。對應的 getter 方法(比如 getOrderList())返回的列表不是活動的,即在返回列表上添加或刪除元素不會導致修改 CriteriaQuery;另外,一些供應商甚至返回不可變的列表以阻止意外使用。因此,良好的實踐是在添加和刪除新的表達式之前,將返回列表復制到一個新的列表中。

根據例子進行查詢

動態查詢 API 中的另一個有用特性就是它能夠輕松地支持根據例子進行查詢。根據例子進行查詢(由 IBM? Research 在 1970 年開發出來)通常被作為早期的軟件終端用戶可用性例子引用。根據例子進行查詢的理念使用模板實例,而不是為查詢指定精確的謂詞。有了給定的模板實例之後,將創建一個聯合謂詞,其中每個謂詞都是模板實例的非 null 和非默認屬性值。執行該查詢將計算謂詞以查找所有與模板實例匹配的實例。根據例子進行查詢曾考慮添加到 JPA 2.0 中,但最終沒有添加。OpenJPA 通過它的擴展 OpenJPAQueryBuilder 接口支持這種查詢,如清單 22 所示:

清單 22. 使用 OpenJPA 的 CriteriaQuery 根據例子進行查詢

CriteriaQuery<Employee> q = cb.createQuery(Employee.class);

Employee example = new Employee();
example.setSalary(10000);
example.setRating(1);

q.where(cb.qbe(q.from(Employee.class), example);

如這個例子所示,OpenJPA 的 QueryBuilder 接口擴展支持以下表達式:

public <T> Predicate qbe(From<?, T> from, T template);

這個表達式根據給定模板實例的屬性值生成一個聯合謂詞。例如,這個查詢將查詢所有薪水為 10000 評級為 1 的 Employee。要進一步控制比較,可以指定不用於比較的可選屬性,以及為值為 String 的屬性指定比較方式。

結束語

本文介紹了 JPA 2.0 中的新 Criteria API,它是一個用 Java 語言開發動態、類型安全的查詢的機制。CriteriaQuery 在運行時被構建為一個強類型查詢表達式樹,本文通過一系列例子展示了它的用法。

本文還確立了 Metamodel API 的關鍵角色,並展示了實例化元模型類如何使編譯器能夠檢查查詢的正確性,從而避免語法有誤的 JPQL 查詢引起的運行時錯誤。除了保證語法正確之外,JPA 2.0 以編程的方式構造查詢的特性還能通過數據庫函數實現更多強大的用途,比如通過例子進行查詢。我希望本文的讀者能夠發現這些強大的新 API 的其他新用途。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved