自從 JPA 於 2006 年首次被引入之後,它就得到了 Java 開發社區的廣泛支持。該規范的下一個主要更新 —— 2.0 版本 (JSR 317) —— 將在 2009 年年底完成。JPA 2.0 引入的關鍵特性之一就是 Criteria API,它為 Java 語言帶來了一種獨特的能力:開發一種 Java 編譯器可以在運行時驗證其正確性的查詢。Criteria API 還提供一個能夠在運行時動態地構建查詢的機制。
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 中的查詢執行模型的以下關鍵方面:
String
(第 2 行)。
EntityManager
是構造一個包含給定 JPQL 字符串的可執行 查詢實例的工廠(第 3 行)。
Java.util.List
的元素。但是這個簡單的例子有一個驗證的錯誤。該代碼能夠順利通過編譯,但將在運行時失敗,因為該 JPQL 查詢字符串的語法有誤。清單 1 的第 2 行的正確語法為:
String jpql = "select p from Person p where p.age > 20";
類型安全查詢如何提供幫助?
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 的核心構造及其基本使用:
EntityManager
實例。
EntityManager
創建 QueryBuilder
的一個實例。QueryBuilder
是 CriteriaQuery
的工廠。
QueryBuilder
工廠構造一個 CriteriaQuery
實例。CriteriaQuery
被賦予泛型類型。泛型參數聲明 CriteriaQuery
在執行時返回的結果的類型。在構造 CriteriaQuery
時,您可以提供各種結果類型參數 —— 從持久化實體(比如 Person.class
)到形式更加靈活的 Object[]
。
CriteriaQuery
實例上設置了查詢表達式。查詢表達式是在一個樹中組裝的核心單元或節點,用於指定 CriteriaQuery
。圖 1 顯示了在 Criteria API 中定義的查詢表達式的層次結構:首先,將 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"));
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 行)。
Person
列表,從而使開發人員在遍歷生成的元素時省去麻煩的強制類型轉換(同時減少了 ClassCastException
運行時錯誤)。現在歸納 清單 2 中的簡單例子的基本方面:
CriteriaQuery
是一個查詢表達式節點樹。在傳統的基於字符串的查詢語言中,這些表達式節點用於指定查詢子句,比如 FROM
、WHERE
和 ORDER BY
。圖 2 顯示了與查詢相關的子句: CriteriaQuery
封裝了傳統查詢的子句Root< T>
,相當於一個 FROM
子句。
Predicate
,其計算為布爾值 true 或 false(事實上,它被聲明為 interface Predicate extends Expression< Boolean>
)。
Path< T>
,表示從 Root< ?>
表達式導航到的持久化屬性。Root< T>
是一個沒有父類的特殊 Path< T>
。QueryBuilder
是 CriteriaQuery
和各種查詢表達式的工廠。
CriteriaQuery
被傳遞給一個可執行查詢並保留類型信息,這樣可以直接訪問選擇列表的元素,而不需要任何運行時強制類型轉換。