詳解JPA 2.0動態(tài)查詢機(jī)制:Criteria API
自從 JPA 于 2006 年首次被引入之后,它就得到了 Java 開發(fā)社區(qū)的廣泛支持。該規(guī)范的下一個主要更新 —— 2.0 版本 (JSR 317) —— 將在 2009 年年底完成。JPA 2.0 引入的關(guān)鍵特性之一就是 Criteria API,它為 Java 語言帶來了一種獨特的能力:開發(fā)一種 Java 編譯器可以在運行時驗證其正確性的查詢。Criteria API 還提供一個能夠在運行時動態(tài)地構(gòu)建查詢的機(jī)制。
#t#本文將介紹 Criteria API 和與之密切相關(guān)的 元模型(metamodel)概念。您將學(xué)習(xí)如何使用 Criteria API 開發(fā) Java 編譯器能夠檢查其正確性的查詢,從而減少運行時錯誤,這種查詢優(yōu)于傳統(tǒng)的基于字符串的 Java Persistence Query Language (JPQL) 查詢。借助使用數(shù)據(jù)庫函數(shù)或匹配模板實例的樣例查詢,我將演示編程式查詢構(gòu)造機(jī)制的強(qiáng)大威力,并將其與使用預(yù)定義語法的 JPQL 查詢進(jìn)行對比。本文假設(shè)您具備基礎(chǔ)的 Java 語言編程知識,并了解常見的 JPA 使用,比如 EntityManagerFactory
或 EntityManager
。
JPQL 查詢有什么缺陷?
JPA 1.0 引進(jìn)了 JPQL,這是一種強(qiáng)大的查詢語言,它在很大程度上導(dǎo)致了 JPA 的流行。不過,基于字符串并使用有限語法的 JPQL 存在一些限制。要理解 JPQL 的主要限制之一,請查看清單 1 中的簡單代碼片段,它通過執(zhí)行 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(); |
這個基礎(chǔ)的例子顯示了 JPA 1.0 中的查詢執(zhí)行模型的以下關(guān)鍵方面:
- JPQL 查詢被指定為一個
String
(第 2 行)。 EntityManager
是構(gòu)造一個包含給定 JPQL 字符串的可執(zhí)行 查詢實例的工廠(第 3 行)。- 查詢執(zhí)行的結(jié)果包含無類型的
java.util.List
的元素。
但是這個簡單的例子有一個驗證的錯誤。該代碼能夠順利通過編譯,但將在運行時失敗,因為該 JPQL 查詢字符串的語法有誤。清單 1 的第 2 行的正確語法為:
String jpql = "select p from Person p where p.age > 20";
|
不幸的是,Java 編譯器不能發(fā)現(xiàn)此類錯誤。在運行時,該錯誤將出現(xiàn)在第 3 或第 4 行(具體行數(shù)取決于 JPA 提供者是否在查詢構(gòu)造或執(zhí)行期間根據(jù) JPQL 語法解析 JPQL 字符串)。
類型安全查詢?nèi)绾翁峁椭?/FONT>
Criteria API 的最大優(yōu)勢之一就是禁止構(gòu)造語法錯誤的查詢。清單 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 的核心構(gòu)造及其基本使用:
- 第 1 行通過幾種可用方法之一獲取一個
EntityManager
實例。 - 在第 2 行,
EntityManager
創(chuàng)建QueryBuilder
的一個實例。QueryBuilder
是CriteriaQuery
的工廠。 - 在第 3 行,
QueryBuilder
工廠構(gòu)造一個CriteriaQuery
實例。CriteriaQuery
被賦予泛型類型。泛型參數(shù)聲明CriteriaQuery
在執(zhí)行時返回的結(jié)果的類型。在構(gòu)造CriteriaQuery
時,您可以提供各種結(jié)果類型參數(shù) —— 從持久化實體(比如Person.class
)到形式更加靈活的Object[]
。 - 第 4 行在
CriteriaQuery
實例上設(shè)置了查詢表達(dá)式。查詢表達(dá)式是在一個樹中組裝的核心單元或節(jié)點,用于指定CriteriaQuery
。圖 1 顯示了在 Criteria API 中定義的查詢表達(dá)式的層次結(jié)構(gòu):
圖 1. 查詢表達(dá)式中的接口層次結(jié)構(gòu)
首先,將
CriteriaQuery
設(shè)置為從Person.class
查詢。結(jié)果返回Root< Person>
實例p
。Root
是一個查詢表達(dá)式,它表示持久化實體的范圍。Root< T>
實際上表示:“對所有類型為T
的實例計算這個查詢?!?這類似于 JPQL 或 SQL 查詢的FROM
子句。另外還需要注意,Root< Person>
是泛型的(實際上每個表達(dá)式都是泛型的)。類型參數(shù)就是表達(dá)式要計算的值的類型。因此Root< Person>
表示一個對Person.class
進(jìn)行計算的表達(dá)式。第 5 行構(gòu)造一個Predicate
。Predicate
是計算結(jié)果為 true 或 false 的常見查詢表達(dá)式形式。謂詞由QueryBuilder
構(gòu)造,QueryBuilder
不僅是CriteriaQuery
的工廠,同時也是查詢表達(dá)式的工廠。QueryBuilder
包含構(gòu)造傳統(tǒng) JPQL 語法支持的所有查詢表達(dá)式的 API 方法,并且還包含額外的方法。在 清單 2 中,QueryBuilder
用于構(gòu)造一個表達(dá)式,它將計算第一個表達(dá)式參數(shù)的值是否大于第二個參數(shù)的值。方法簽名為: -
Predicate gt(Expression< ? extends Number> x, Number y);
這個方法簽名是展示使用強(qiáng)類型語言(比如 Java)定義能夠檢查正確性并阻止錯誤的 API 的好例子。該方法簽名指定,僅能將值為
Number
的表達(dá)式與另一個值也為Number
的表達(dá)式進(jìn)行比較(例如,不能與值為String
的表達(dá)式進(jìn)行比較):Predicate condition = qb.gt(p.get(Person_.age), 20);
第 5 行有更多學(xué)問。注意
qb.gt()
方法的第一個輸入?yún)?shù):p.get(Person_.age)
,其中p
是先前獲得的Root< Person>
表達(dá)式。p.get(Person_.age)
是一個路徑表達(dá)式。路徑表達(dá)式是通過一個或多個持久化屬性從根表達(dá)式進(jìn)行導(dǎo)航得到的結(jié)果。因此,表達(dá)式p.get(Person_.age)
表示使用Person
的age
屬性從根表達(dá)式p
導(dǎo)航。您可能不明白Person_.age
是什么。您可以將其暫時看作一種表示Person
的age
屬性的方法。我將在談?wù)?JPA 2.0 引入的新 Metamodel API 時詳細(xì)解釋Person_.age
。如前所述,每個查詢表達(dá)式都是泛型的,以表示表達(dá)式計算的值的類型。如果
Person.class
中的age
屬性被聲明為類型Integer
(或int
),則表達(dá)式p.get(Person_.age)
的計算結(jié)果的類型為Integer
。由于 API 中的類型安全繼承,編輯器本身將對無意義的比較拋出錯誤,比如:Predicate condition = qb.gt(p.get(Person_.age, "xyz"));
- 第 6 行在
CriteriaQuery
上將謂詞設(shè)置為其WHERE
子句。 -
在第 7 行中,
EntityManager
創(chuàng)建一個可執(zhí)行查詢,其輸入為CriteriaQuery
。這類似于構(gòu)造一個輸入為 JPQL 字符串的可執(zhí)行查詢。但是由于輸入CriteriaQuery
包含更多的類型信息,所以得到的結(jié)果是TypedQuery
,它是熟悉的javax.persistence.Query
的一個擴(kuò)展。如其名所示,TypedQuery
知道執(zhí)行它返回的結(jié)果的類型。它是這樣定義的:public interface TypedQuery< T> extends Query { List< T> getResultList(); }
與對應(yīng)的無類型超接口相反:
public interface Query { List getResultList(); }
很明顯,
TypedQuery
結(jié)果具有相同的Person.class
類型,該類型在構(gòu)造輸入CriteriaQuery
時由QueryBuilder
指定(第 3 行)。 - 在第 8 行中,當(dāng)最終執(zhí)行查詢以獲得結(jié)果列表時,攜帶的類型信息展示了其優(yōu)勢。得到的結(jié)果是帶有類型的
Person
列表,從而使開發(fā)人員在遍歷生成的元素時省去麻煩的強(qiáng)制類型轉(zhuǎn)換(同時減少了ClassCastException
運行時錯誤)。
現(xiàn)在歸納 清單 2 中的簡單例子的基本方面:
CriteriaQuery
是一個查詢表達(dá)式節(jié)點樹。在傳統(tǒng)的基于字符串的查詢語言中,這些表達(dá)式節(jié)點用于指定查詢子句,比如FROM
、WHERE
和ORDER BY
。圖 2 顯示了與查詢相關(guān)的子句:
圖 2.CriteriaQuery
封裝了傳統(tǒng)查詢的子句
- 查詢表達(dá)式被賦予泛型。一些典型的表達(dá)式是:
Root< T>
,相當(dāng)于一個FROM
子句。Predicate
,其計算為布爾值 true 或 false(事實上,它被聲明為interface Predicate extends Expression< Boolean>
)。Path< T>
,表示從Root< ?>
表達(dá)式導(dǎo)航到的持久化屬性。Root< T>
是一個沒有父類的特殊Path< T>
。
QueryBuilder
是CriteriaQuery
和各種查詢表達(dá)式的工廠。CriteriaQuery
被傳遞給一個可執(zhí)行查詢并保留類型信息,這樣可以直接訪問選擇列表的元素,而不需要任何運行時強(qiáng)制類型轉(zhuǎn)換。
#p#
持久化域的元模型
討論 清單 2 時指出了一個不常見的構(gòu)造:Person_.age
,它表示 Person
的持久化屬性 age
。清單 2 使用 Person_.age
形成一個路徑表達(dá)式,它通過 p.get(Person_.age)
從 Root< Person>
表達(dá)式 p
導(dǎo)航而來。Person_.age
是 Person_
類中的公共靜態(tài)字段,Person_
是靜態(tài)、已實例化的規(guī)范元模型類,對應(yīng)于原來的 Person
實體類。
元模型類描述持久化類的元數(shù)據(jù)。如果一個類安裝 JPA 2.0 規(guī)范精確地描述持久化實體的元數(shù)據(jù),那么該元模型類就是規(guī)范的。規(guī)范的元模型類是靜態(tài)的,因此它的所有成員變量都被聲明為靜態(tài)的(也是 public
的)。Person_.age
是靜態(tài)成員變量之一。您可以在開發(fā)時在源代碼中生成一個具體的 Person_.java
來實例化 一個規(guī)范類。實例化之后,它就可以在編譯期間以強(qiáng)類型的方式引用 Person
的持久化屬性。
這個 Person
_metamodel 類是引用 Person
的元信息的一種代替方法。這種方法類似于經(jīng)常使用(有人可能認(rèn)為是濫用)的 Java Reflection API,但概念上有很大的不同。您可以使用反射獲得關(guān)于 java.lang.Class
的實例的元信息,但是不能以編譯器能夠檢查的方式引用關(guān)于 Person.class
的元信息。例如,使用反射時,您將這樣引用 Person.class
中的 age
字段:
Field field = Person.class.getField("age"); |
不過,這種方法也存在很大的限制,類似于 清單 1 中基于字符串的 JPQL 查詢存在的限制。編譯器能夠順利編譯該代碼,但不能確定它是否可以正常工作。如果該代碼包含任何錯誤輸入,它在運行時肯定會失敗。反射不能實現(xiàn) JPA 2.0 的類型安全查詢 API 要實現(xiàn)的功能。
類型安全查詢 API 必須讓您的代碼能夠引用 Person
類中的持久化屬性 age
,同時讓編譯器能夠在編譯期間檢查錯誤。JPA 2.0 提供的解決辦法通過靜態(tài)地公開相同的持久化屬性實例化名為 Person_
的元模型類(對應(yīng)于 Person
)。
關(guān)于元信息的討論通常都是令人昏昏欲睡的。所以我將為熟悉的 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 提供者能夠?qū)⑦@個類的實例作為持久化實體管理。
清單 4 顯示了 domain.Person
的對應(yīng)靜態(tài)規(guī)范元模型類:
清單 4. 一個簡單實體的規(guī)范元模型
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,?>
的靜態(tài)公共字段。通過利用這個 Person_
元模型類,可以在編譯期間引用 domain.Person
的持久化屬性 age
— 不是通過 Reflection API,而是直接引用靜態(tài)的 Person_.age
字段。然后,編譯器可以根據(jù) age
屬性聲明的類型實施類型檢查。我已經(jīng)列舉了一個關(guān)于此類限制的例子:QueryBuilder.gt(p.get(Person_.age), "xyz")
將導(dǎo)致編譯器錯誤,因為編譯器通過 QueryBuilder.gt(..)
的簽名和 Person_.age
的類型可以確定 Person
的 age
屬性是一個數(shù)字字段,不能與 String
進(jìn)行比較。
其他一些需要注意的要點包括:
- 元模型
Person_.age
字段被聲明為類型javax.persistence.metamodel.SingularAttribute
。SingularAttribute
是 JPA Metamodel API 中定義的接口之一,我將在下一小節(jié)描述它。SingularAttribute< Person, Integer>
的泛型參數(shù)表示該類聲明原來的持久化屬性和持久化屬性本身的類型。 - 元模型類被注釋為
@StaticMetamodel(domain.Person.class)
以將其標(biāo)記為一個與原來的持久化domain.Person
實體對應(yīng)的元模型類。
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 中的持久化類型的接口的層次結(jié)構(gòu)
圖 4 顯示了在 Metamodel API 中定義用于描述屬性的接口:
圖 4. Metamodel API 中的持久化屬性的接口的層次結(jié)構(gòu)
JPA 的 Metamodel API 接口比 Java Reflection API 更加專業(yè)化。需要更細(xì)微的差別來表達(dá)關(guān)于持久化的豐富元信息。例如,Java Reflection API 將所有 Java 類型表示為 java.lang.Class
。即沒有通過獨立的定義對概念進(jìn)行區(qū)分,比如類、抽象類和接口。當(dāng)然,您可以詢問 Class
它是一個接口還是一個抽象類,但這與通過兩個獨立的定義表示接口和抽象類的差別不同。
Java Reflection API 在 Java 語言誕生時就被引入(對于一種常見的多用途編程語言而言,這曾經(jīng)是一個非常前沿的概念),但是經(jīng)過多年的發(fā)展才認(rèn)識到強(qiáng)類型系統(tǒng)的用途和強(qiáng)大之處。JPA Metamodel API 將強(qiáng)類型引入到持久化實體中。例如,持久化實體在語義上區(qū)分為 MappedSuperClass
、Entity
和 Embeddable
。在 JPA 2.0 之前,這種語義區(qū)分是通過持久化類定義中的對應(yīng)類級別注釋來表示的。JPA Metamodel 在 javax.persistence.metamodel
包中描述了 3 個獨立的接口( MappedSuperclassType
、EntityType
和 EmbeddableType
),以更加鮮明的對比它們的語義特征。類似地,可以通過接口(比如 SingularAttribute
、CollectionAttribute
和 MapAttribute
)在類型定義級別上區(qū)分持久化屬性。
除了方便描述之外,這些專門化的元模型接口還有實用優(yōu)勢,能夠幫助構(gòu)建類型安全的查詢從而減少運行時錯誤。您在前面的例子中看到了一部分優(yōu)勢,隨著我通過 CriteriaQuery
描述關(guān)于連接的例子,您將看到更多優(yōu)勢。
運行時作用域
一般而言,可以將 Java Reflection API 的傳統(tǒng)接口與專門用于描述持久化元數(shù)據(jù)的 javax.persistence.metamodel
的接口進(jìn)行比較。要進(jìn)一步進(jìn)行類比,則需要對元模型接口使用等效的運行時作用域概念。java.lang.Class
實例的作用域由 java.lang.ClassLoader
在運行時劃分。一組相互引用的 Java 類實例必須在 ClassLoader
作用域下定義。作用域的邊界是嚴(yán)格 或封閉 的,如果在 ClassLoader L
作用域下定義的類 A
試圖引用不在 ClassLoader L
作用域之內(nèi)的類 B
,結(jié)果將收到可怕的 ClassNotFoundException
或 NoClassDef FoundError
(對于處理包含多個 ClassLoader
的環(huán)境的開發(fā)人員或部署人員而言,問題就復(fù)雜了)。
現(xiàn)在將一組嚴(yán)格的可相互引用的類稱為運行時作用域,而在 JPA 1.0 中稱為持久化單元。持久化單元作用域的持久化實體在 META-INF/persistence.xml 文件的 < class>
子句中枚舉。在 JPA 2.0 中,通過 javax.persistence.metamodel.Metamodel
接口讓開發(fā)人員可以在運行時使用作用域。Metamodel
接口是特定持久化單元知道的所有持久化實體的容器,如圖 5 所示:
圖 5. 元模型接口是持久化單元中的類型的容器
這個接口允許通過元模型元素的對應(yīng)持久化實體類訪問元模型元素。例如,要獲得對 Person
持久化實體的持久化元數(shù)據(jù)的引用,可以編寫:
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
實體中聲明的持久化屬性。如果應(yīng)用程序在 pClass
(比如 pClass.getSingularAttribute("age", Integer.class)
)上調(diào)用一個方法,它將返回一個 SingularAttribute< Person, Integer>
實例,該實例與實例化規(guī)范元模型類的靜態(tài) Person_.age
成員相同。最重要的是,對于應(yīng)用程序可以通過 Metamodel API 在運行時引用的屬性,是通過實例化靜態(tài)規(guī)范元模型 Person_
類向 Java 編譯器提供的。
除了將持久化實體分解為對應(yīng)的元模型元素之外,Metamodel API 還允許訪問所有已知的元模型類 (Metamodel.getManagedTypes()
),或者通過類的持久化信息訪問元模型類,例如 embeddable(Address.class)
,它將返回一個 EmbeddableType< Address>
實例(ManagedType< >
的子接口)。
在 JPA 中,關(guān)于 POJO 的元信息使用帶有源代碼注釋(或 XML 描述符)的持久化元信息進(jìn)一步進(jìn)行區(qū)分 —— 比如類是否是嵌入的,或者哪個字段用作主鍵。持久化元信息分為兩大類:持久化(比如 @Entity
)和映射(比如 @Table
)。在 JPA 2.0 中,元模型僅為持久化注釋(不是映射注釋)捕捉元數(shù)據(jù)。因此,使用當(dāng)前版本的 Metamodel API 可以知道哪些字段是持久化的,但不能找到它們映射到的數(shù)據(jù)庫列。
規(guī)范和非規(guī)范
盡管 JPA 2.0 規(guī)范規(guī)定了規(guī)范的靜態(tài)元模型類的精確樣式(包括元模型類的完整限定名及其靜態(tài)字段的名稱),應(yīng)用程序也能夠編寫這些元模型類。如果應(yīng)用程序開發(fā)人員編寫元模型類,這些類就稱為非規(guī)范元模型?,F(xiàn)在,關(guān)于非規(guī)范元模型的規(guī)范還不是很詳細(xì),因此對非規(guī)范元模型的支持不能在 JPA 提供者之間移植。您可能已經(jīng)注意到,公共靜態(tài)字段僅在規(guī)范元模型中聲明,而沒有初始化。聲明之后就可以在開發(fā) CriteriaQuery
時引用這些字段。但是,必須在運行時給它們賦值才有意義。盡管為規(guī)范元模型的字段賦值是 JPA 提供者的責(zé)任,但非規(guī)范元模型則不存在這一要求。使用非規(guī)范元模型的應(yīng)用程序必須依賴于特定供應(yīng)商機(jī)制,或開發(fā)自己的機(jī)制來在運行時初始化元模型屬性的字段值。
注釋處理和元模型生成
如果您有許多持久化實體,您將傾向于不親自編寫元模型類,這是很自然的事情。持久化提供者應(yīng)該 為您生成這些元模型類。在規(guī)范中沒有強(qiáng)制規(guī)定這種工具或生成機(jī)制,但是 JPA 之間已經(jīng)私下達(dá)成共識,他們將使用在 Java 6 編譯器中集成的 Annotation Processor 工具生成規(guī)范元模型。Apache OpenJPA 提供一個工具來生成這些元模型類,其生成方式有兩種,一是在您為持久化實體編譯源代碼時隱式地生成,二是通過顯式地調(diào)用腳本生成。在 Java 6 以前,有一個被廣泛使用的稱為 apt
的 Annotation Processor 工具,但在 Java 6 中,編譯器和 Annotation Processor 的合并被定義為標(biāo)準(zhǔn)的一部分。
要像持久化提供者一樣在 OpenJPA 中生成這些元模型類,僅需在編譯器的類路徑中使用 OpenJPA 類庫編譯 POJO 實體:
$ javac domain/Person.java |
將生成規(guī)范元模型 Person_
類,它將位于 Person.java 所在的目錄,并且作為該編譯的一部分。
編寫類型安全的查詢
到目前為止,我已經(jīng)構(gòu)建了 CriteriaQuery
的組件和相關(guān)的元模型類?,F(xiàn)在,我將展示如何使用 Criteria API 開發(fā)一些查詢。
函數(shù)表達(dá)式
函數(shù)表達(dá)式將一個函數(shù)應(yīng)用到一個或多個輸入?yún)?shù)以創(chuàng)建新的表達(dá)式。函數(shù)表達(dá)式的類型取決于函數(shù)的性質(zhì)及其參數(shù)的類型。輸入?yún)?shù)本身可以是表達(dá)式或文本值。編譯器的類型檢查規(guī)則與 API 簽名結(jié)合確定什么是合法輸入。
考慮一個對輸入表達(dá)式應(yīng)用平均值的單參數(shù)表達(dá)式。CriteriaQuery
選擇所有 Account
的平均余額,如清單 5 所示:
清單 5. CriteriaQuery
中的函數(shù)表達(dá)式
CriteriaQuery< Double> c = cb.createQuery(Double.class); Root< Account> a = c.from(Account.class); c.select(cb.avg(a.get(Account_.balance))); |
等效的 JPQL 查詢?yōu)椋?/P>
String jpql = "select avg(a.balance) from Account a";
|
在 清單 5 中,QueryBuilder
工廠(由變量 cb
表示)創(chuàng)建一個 avg()
表達(dá)式,并將其用于查詢的 select()
子句。
該查詢表達(dá)式是一個構(gòu)建塊,可以通過組裝它為查詢定義最后的選擇謂詞。清單 6 中的例子顯示了通過導(dǎo)航到 Account
的余額創(chuàng)建的 Path
表達(dá)式,然后 Path
表達(dá)式被用作兩個二進(jìn)制函數(shù)表達(dá)式( greaterThan()
和 lessThan()
)的輸入表達(dá)式,這兩個表達(dá)式的結(jié)果都是一個布爾表達(dá)式或一個謂詞。然后,通過 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 查詢?yōu)椋?/P>
"select a from Account a where a.balance>100 and a.balance< 200"; |
符合謂詞
某些表達(dá)式(比如 in()
)可以應(yīng)用到多個表達(dá)式。清單 7 給出了一個例子:
清單 7. CriteriaQuery
中的多值表達(dá)式
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
進(jìn)行導(dǎo)航,創(chuàng)建一個表示帳戶所有者的名稱的路徑。然后,它創(chuàng)建一個使用路徑表達(dá)式作為輸入的 in()
表達(dá)式。in()
表達(dá)式計算它的輸入表達(dá)式是否等于它的參數(shù)之一。這些參數(shù)通過 value()
方法在 In< T>
表達(dá)式上指定,In< T>
的簽名如下所示:
In< T> value(T value); |
注意如何使用 Java 泛型指定僅對值的類型為 T
的成員計算 In< T>
表達(dá)式。因為表示 Account
所有者的名稱的路徑表達(dá)式的類型為 String
,所以與值為 String
類型的參數(shù)進(jìn)行比較才有效,String
值參數(shù)可以是字面量或計算結(jié)果為 String
的另一個表達(dá)式。
將 清單 7 中的查詢與等效(正確)的 JPQL 進(jìn)行比較:
"select a from Account a where a.owner.name in ('X','Y','Z')"; |
在 JPQL 中的輕微疏忽不僅不會被編輯器檢查到,它還可能導(dǎo)致意外結(jié)果。例如:
"select a from Account a where a.owner.name in (X, Y, Z)"; |
連接關(guān)系
盡管 清單 6 和 清單 7 中的例子將表達(dá)式用作構(gòu)建塊,查詢都是基于一個實體及其屬性之上的。但是查詢通常涉及到多個實體,這就要求您將多個實體連接 起來。CriteriaQuery
通過類型連接表達(dá)式 連接兩個實體。類型連接表達(dá)式有兩個類型參數(shù):連接源的類型和連接目標(biāo)屬性的可綁定類型。例如,如果您想查詢有一個或多個 PurchaseOrder
沒有發(fā)出的 Customer
,則需要通過一個表達(dá)式將 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); |
連接表達(dá)式從根表達(dá)式 c
創(chuàng)建,持久化屬性 Customer.orders
由連接源(Customer
)和 Customer.orders
屬性的可綁定類型進(jìn)行參數(shù)化,可綁定類型是 PurchaseOrder
而不是 已聲明的類型 java.util.Set< PurchaseOrder>
。此外還要注意,因為初始屬性的類型為 java.util.Set
,所以生成的連接表達(dá)式為 SetJoin
,它是專門針對類型被聲明為 java.util.Set
的屬性的 Join
。類似地,對于其他受支持的多值持久化屬性類型,該 API 定義 CollectionJoin
、ListJoin
和 MapJoin
。(圖 1 顯示了各種連接表達(dá)式)。在 清單 8 的第 3 行不需要進(jìn)行顯式的轉(zhuǎn)換,因為 CriteriaQuery
和 Metamodel API 通過覆蓋 join()
的方法能夠識別和區(qū)分聲明為 java.util.Collection
或 List
或者 Set
或 Map
的屬性類型。
在查詢中使用連接在連接實體上形成一個謂詞。因此,如果您想要選擇有一個或多個未發(fā)送 PurchaseOrder
的 Customer
,可以通過狀態(tài)屬性從連接表達(dá)式 o
進(jìn)行導(dǎo)航,然后將其與 DELIVERED
狀態(tài)比較,并否定謂詞:
Predicate p = cb.equal(o.get(PurchaseOrder_.status), Status.DELIVERED) .negate(); |
創(chuàng)建連接表達(dá)式需要注意的一個地方是,每次連接一個表達(dá)式時,都會返回一個新的表達(dá)式,如清單 9 所示:
清單 9. 每次連接創(chuàng)建一個唯一的實例
SetJoin< Customer, PurchaseOrder> o1 = c.join(Customer_.orders); SetJoin< Customer, PurchaseOrder> o2 = c.join(Customer_.orders); assert o1 == o2; |
清單 9 中對兩個來自相同表達(dá)式 c
的連接表達(dá)式的等同性斷言將失敗。因此,如果查詢的謂詞涉及到未發(fā)送并且值大于 $200 的 PurchaseOrder
,那么正確的構(gòu)造是將 PurchaseOrder
與根 Customer
表達(dá)式連接起來(僅一次),把生成的連接表達(dá)式分配給本地變量(等效于 JPQL 中的范圍變量),并在構(gòu)成謂詞時使用本地變量。
使用參數(shù)
回顧一下本文初始的 JPQL 查詢(正確那個):
String jpql = "select p from Person p where p.age > 20";
|
盡管編寫查詢時通常包含常量文本值,但這不是一個良好實踐。良好實踐是參數(shù)化查詢,從而僅解析或準(zhǔn)備查詢一次,然后再緩存并重用它。因此,編寫查詢的最好方法是使用命名參數(shù):
String jpql = "select p from Person p where p.age > :age";
|
參數(shù)化查詢在查詢執(zhí)行之前綁定參數(shù)的值:
Query query = em.createQuery(jpql).setParameter("age", 20); List result = query.getResultList(); |
在 JPQL 查詢中,查詢字符串中的參數(shù)以命名方式(前面帶有冒號,例如 :age
)或位置方式(前面帶有問號,例如 ?3
)編碼。在 CriteriaQuery
中,參數(shù)本身就是查詢表達(dá)式。與其他表達(dá)式一樣,它們是強(qiáng)類型的,并且由表達(dá)式工廠(即 QueryBuilder
)構(gòu)造。然后,可以參數(shù)化 清單 2 中的查詢,如清單 10 所示:
清單 10. 在 CriteriaQuery
中使用參數(shù)
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(); |
比較該參數(shù)使用和 JPQL 中的參數(shù)使用:參數(shù)表達(dá)式被創(chuàng)建為帶有顯式類型信息 Integer
,并且被直接用于將值 20
綁定到可執(zhí)行查詢。額外的類型信息對減少運行時錯誤十分有用,因為阻止參數(shù)與包含不兼容類型的表達(dá)式比較,或阻止參數(shù)與不兼容類型的值綁定。JPQL 查詢的參數(shù)不能提供任何編譯時安全。
清單 10 中的例子顯示了一個直接用于綁定的未命名表達(dá)式。還可以在構(gòu)造參數(shù)期間為參數(shù)分配第二個名稱。對于這種情況,您可以使用這個名稱將參數(shù)值綁定到查詢。不過,您不可以使用位置參數(shù)。線性 JPQL 查詢字符串中的整數(shù)位置有一定的意義,但是不能在概念模型為查詢表達(dá)式樹的 CriteriaQuery
上下文中使用整數(shù)位置。
JPA 查詢參數(shù)的另一個有趣方面是它們沒有內(nèi)部值。值綁定到可執(zhí)行查詢上下文中的參數(shù)。因此,可以合法地從相同的 CriteriaQuery
創(chuàng)建兩個獨立可執(zhí)行的查詢,并為這些可執(zhí)行查詢的相同參數(shù)綁定兩個整數(shù)值。
預(yù)測結(jié)果
您已經(jīng)看到 CriteriaQuery
在執(zhí)行時返回的結(jié)果已經(jīng)在 QueryBuilder
構(gòu)造 CriteriaQuery
時指定。查詢的結(jié)果被指定為一個或多個預(yù)測條件??梢酝ㄟ^兩種方式之一在 CriteriaQuery
接口上指定預(yù)測條件:
CriteriaQuery< T> select(Selection< ? extends T> selection); CriteriaQuery< T> multiselect(Selection< ?>... selections); |
最簡單并且最常用的預(yù)測條件是查詢候選類。它可以是隱式的,如清單 11 所示:
清單 11. CriteriaQuery
默認(rèn)選擇的候選區(qū)段
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(); |
如果查詢的預(yù)測結(jié)果不是候選持久化實體本身,那么可以通過其他幾個構(gòu)造方法來生成查詢的結(jié)果。這些構(gòu)造方法包含在 QueryBuilder
接口中,如清單 13 所示:
清單 13. 生成查詢結(jié)果的方法
< Y> CompoundSelection< Y> construct(Class< Y> result, Selection< ?>... terms); CompoundSelection< Object[]> array(Selection< ?>... terms); CompoundSelection< Tuple> tuple(Selection< ?>... terms); |
清單 13 中的方法構(gòu)建了一個由其他幾個可選擇的表達(dá)式組成的預(yù)測條件。construct()
方法創(chuàng)建給定類參數(shù)的一個實例,并使用來自輸入選擇條件的值調(diào)用一個構(gòu)造函數(shù)。例如,如果 CustomerDetails
— 一個非持久化實體 — 有一個接受 String
和 int
參數(shù)的構(gòu)造方法,那么 CriteriaQuery
可以通過從選擇的 Customer
— 一個持久化實體 — 實例的名稱和年齡創(chuàng)建實例,從而返回 CustomerDetails
作為它的結(jié)果,如清單 14 所示:
清單 14. 通過 construct()
將查詢結(jié)果包放入類的實例
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)); |
可以將多個預(yù)測條件合并在一起,以組成一個表示 Object[]
或 Tuple
的復(fù)合條件。清單 15 顯示了如何將結(jié)果包裝到 Object[]
中:
清單 15. 將結(jié)果包裝到 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(); |
這個查詢返回一個結(jié)果列表,它的每個元素都是一個長度為 2 的 Object[]
,第 0 個數(shù)組元素為 Customer
的名稱,第 1 個數(shù)組元素為 Customer
的年齡。
Tuple
是一個表示一行數(shù)據(jù)的 JPA 定義接口。從概念上看,Tuple
是一個 TupleElement
列表 — 其中 TupleElement
是源自單元和所有查詢表達(dá)式的根。包含在 Tuple
中的值可以被基于 0 的整數(shù)索引訪問(類似于熟悉的 JDBC 結(jié)果),也可以被 TupleElement
的別名訪問,或直接通過 TupleElement
訪問。清單 16 顯示了如何將結(jié)果包裝到 Tuple
中:
清單 16. 將查詢結(jié)果包裝到 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); |
這個查詢返回一個結(jié)果列表,它的每個元素都是一個 Tuple
。反過來,每個二元組都帶有兩個元素 — 可以被每個 TupleElement
的索引或別名(如果有的話)訪問,或直接被 TupleElement
訪問。清單 16 中需要注意的兩點是 alias()
的使用,它是將一個名稱綁定到查詢表達(dá)式的一種方式(創(chuàng)建一個新的副本),和 QueryBuilder
上的 createTupleQuery()
方法,它僅是 createQuery(Tuple.class)
的代替物。
這些能夠改變結(jié)果的方法的行為和在構(gòu)造期間被指定為 CriteriaQuery
的類型參數(shù)結(jié)果共同組成 multiselect()
方法的語義。這個方法根據(jù)最終實現(xiàn)結(jié)果的 CriteriaQuery
的結(jié)果類型解釋它的輸入條件。要像 清單 14 一樣使用 multiselect()
構(gòu)造 CustomerDetails
實例,您需要將 CriteriaQuery
的類型指定為 CustomerDetails
,然后使用將組成 CustomerDetails
構(gòu)造方法的條件調(diào)用 multiselect()
,如清單 17 所示:
清單 17. 基于結(jié)果類型的 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)); |
因為查詢結(jié)果類型為 CustomerDetails
,multiselect()
將其預(yù)測條件解釋為 CustomerDetails
構(gòu)造方法參數(shù)。如將查詢指定為返回 Tuple
,那么帶有相同參數(shù)的 multiselect()
方法將創(chuàng)建 Tuple
實例,如清單 18 所示:
清單 18. 使用 multiselect()
方法創(chuàng)建 Tuple
實例
CriteriaQuery< Tuple> q = cb.createTupleQuery(); Root< Customer> c = q.from(Customer.class); q.multiselect(c.get(Customer_.name), c.get(Customer_.age)); |
如果以 Object
作為結(jié)果類型或沒有指定類型參數(shù)時,multiselect()
的行為會變得更加有趣。在這些情況中,如果 multiselect()
使用單個輸入條件,那么返回值將為所選擇的條件。但是如果 multiselect()
包含多個輸入條件,結(jié)果將得到一個 Object[]
。
#p#
高級特性
到目前為止,我主要強(qiáng)調(diào)了 Criteria API 的強(qiáng)類型,以及它如何幫助減少出現(xiàn)在基于字符串 JPQL 查詢中的語義錯誤。Criteria API 還是以編程的方式構(gòu)建查詢的機(jī)制,因此通常被稱為動態(tài) 查詢 API。編程式查詢構(gòu)造 API 的威力是無窮的,但它的利用還取決于用戶的創(chuàng)造能力。我將展示 4 個例子:
- 使用弱類型的 API 構(gòu)建動態(tài)查詢
- 使用數(shù)據(jù)庫支持的函數(shù)作為查詢表達(dá)式來擴(kuò)展語法
- 編輯查詢實現(xiàn) “在結(jié)果中搜索” 功能
- 根據(jù)例子進(jìn)行查詢 — 數(shù)據(jù)庫社區(qū)熟悉的模式
弱類型和動態(tài)查詢構(gòu)建
Criteria API 的強(qiáng)類型檢查基于開放期間的實例化元模型類的可用性。不過,在某些情況下,選擇的實體僅能夠在運行時決定。為了支持這種用法,Criteria API 方法提供一個并列版本,其中持久化屬性通過它們的名稱進(jìn)行引用(類似于 Java Reflection API),而不是引用實例化靜態(tài)元模型屬性。該 API 的這個并列版本可以通過犧牲編譯時類型檢查來真正地支持動態(tài)查詢構(gòu)造。清單 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 不能夠返回正確的泛型表達(dá)式,因此生成一個編輯器來警告未檢查的轉(zhuǎn)換。一種消除這些煩人的警告消息的方法是使用 Java 泛型不常用的工具:參數(shù)化方法調(diào)用,比如 清單 19 中通過調(diào)用 get()
方法獲取路徑表達(dá)式。
可擴(kuò)展數(shù)據(jù)庫表達(dá)式
動態(tài)查詢構(gòu)造機(jī)制的獨特優(yōu)勢是它的語法是可擴(kuò)展的。例如,您可以在 QueryBuilder
接口中使用 function()
方法創(chuàng)建數(shù)據(jù)庫支持的表達(dá)式:
< T> Expression< T> function(String name, Class< T> type, Expression< ?>...args); |
function()
方法創(chuàng)建一個帶有給定名稱和 0 個或多個輸入表達(dá)式的表達(dá)式。function()
表達(dá)式的計算結(jié)果為給定的類型。這允許應(yīng)用程序創(chuàng)建一個計算數(shù)據(jù)庫的查詢。例如,MySQL 數(shù)據(jù)庫支持 CURRENT_USER()
函數(shù),它為服務(wù)器用于驗證當(dāng)前客戶機(jī)的 MySQL 帳戶返回一個由用戶名和主機(jī)名組成的 UTF-8 字符串。應(yīng)用程序可以在 CriteriaQuery
中使用未帶參數(shù)的 CURRENT_USER()
函數(shù),如清單 20 所示:
清單 20. 在 CriteriaQuery
中使用特定于數(shù)據(jù)庫的函數(shù)
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 中不能表達(dá)等效的查詢,因為它的語法僅支持固定數(shù)量的表達(dá)式。動態(tài) API 不受固定數(shù)量表達(dá)式的嚴(yán)格限制。
可編輯查詢
可以以編程的方式編輯 CriteriaQuery
??梢愿淖儾樵兊淖泳?,比如它的選擇條件、WHERE
子句中的選擇謂詞和 ORDER BY
子句中的排序條件。可以在典型的 “在結(jié)果中搜索” 工具中使用這個編輯功能,以添加更多限制在后續(xù)步驟中進(jìn)一步細(xì)化查詢謂詞。
清單 21 中的例子創(chuàng)建了一個根據(jù)名稱對結(jié)果進(jìn)行排序的查詢,然后編輯該查詢以根據(jù)郵政編碼進(jìn)行查詢:
清單 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(); |
在 CriteriaQuery
上的 setter 方法 — select()
、where()
或 orderBy()
— 使用新的參數(shù)替換先前的值。對應(yīng)的 getter 方法(比如 getOrderList()
)返回的列表不是活動的,即在返回列表上添加或刪除元素不會導(dǎo)致修改 CriteriaQuery
;另外,一些供應(yīng)商甚至返回不可變的列表以阻止意外使用。因此,良好的實踐是在添加和刪除新的表達(dá)式之前,將返回列表復(fù)制到一個新的列表中。
根據(jù)例子進(jìn)行查詢
動態(tài)查詢 API 中的另一個有用特性就是它能夠輕松地支持根據(jù)例子進(jìn)行查詢。根據(jù)例子進(jìn)行查詢(由 IBM® Research 在 1970 年開發(fā)出來)通常被作為早期的軟件終端用戶可用性例子引用。根據(jù)例子進(jìn)行查詢的理念使用模板實例,而不是為查詢指定精確的謂詞。有了給定的模板實例之后,將創(chuàng)建一個聯(lián)合謂詞,其中每個謂詞都是模板實例的非 null 和非默認(rèn)屬性值。執(zhí)行該查詢將計算謂詞以查找所有與模板實例匹配的實例。根據(jù)例子進(jìn)行查詢曾考慮添加到 JPA 2.0 中,但最終沒有添加。OpenJPA 通過它的擴(kuò)展 OpenJPAQueryBuilder
接口支持這種查詢,如清單 22 所示:
清單 22. 使用 OpenJPA 的 CriteriaQuery
根據(jù)例子進(jìn)行查詢
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
接口擴(kuò)展支持以下表達(dá)式:
public < T> Predicate qbe(From< ?, T> from, T template); |
這個表達(dá)式根據(jù)給定模板實例的屬性值生成一個聯(lián)合謂詞。例如,這個查詢將查詢所有薪水為 10000
評級為 1
的 Employee
。要進(jìn)一步控制比較,可以指定不用于比較的可選屬性,以及為值為 String
的屬性指定比較方式。
結(jié)束語
本文介紹了 JPA 2.0 中的新 Criteria API,它是一個用 Java 語言開發(fā)動態(tài)、類型安全的查詢的機(jī)制。CriteriaQuery
在運行時被構(gòu)建為一個強(qiáng)類型查詢表達(dá)式樹,本文通過一系列例子展示了它的用法。
本文還確立了 Metamodel API 的關(guān)鍵角色,并展示了實例化元模型類如何使編譯器能夠檢查查詢的正確性,從而避免語法有誤的 JPQL 查詢引起的運行時錯誤。除了保證語法正確之外,JPA 2.0 以編程的方式構(gòu)造查詢的特性還能通過數(shù)據(jù)庫函數(shù)實現(xiàn)更多強(qiáng)大的用途,比如通過例子進(jìn)行查詢。我希望本文的讀者能夠發(fā)現(xiàn)這些強(qiáng)大的新 API 的其他新用途。