Hibernate ORM 用戶手冊 第16章 Criteria
Criteria查詢提供了一種類型安全的針對HQL、JPQL和原生SQL查詢的替代方案。
Hibernate曾經提供了一種舊的、傳統的
org.hibernate.Criteria
API,現在應該當做過期功能對待。它們將不再收到任何新功能開發。最終,Hibernate特有的Criteria查詢功能會移植做爲JPAjavax.persistence.criteria.CriteriaQuery
的擴展。要了解org.hibernate.Criteria
API的相關內容,請參考《傳統Hibernate Criteria查詢》。本章將集中討論基於JPA API來聲明類型安全的Criteria查詢。
Criteria查詢是一種編程式、類型安全的用於表達查詢的手段。因爲它使用接口和類表示查詢的各種結構部分(例如查詢本身,select子句或order-by等),所以是類型安全的。此外,屬性的引用也可以是類型安全的,我們稍後會看到。使用過舊的Hibernate org.hibernate.Criteria
API的用戶會發現其實JPA API和前者的目的是一致的,但是我們相信JPA API更加優越。這種信念是因爲JPA API清晰地流露出了它從其他API中學到的經驗。
Criteria查詢本質上是一個類圖,類圖的每部分都代表了這個查詢中的一部分原子性增量(在向下瀏覽類圖的時候)。要想開啓一個Criteria查詢,第一步是就是構建此類圖。使用Criteria查詢時首先要熟悉接口javax.persistence.criteria.CriteriaBuilder
。這是一個用於構建Criteria查詢中每個獨立部件的工廠。我們可以通過javax.persistence.EntityManagerFactory
或javax.persistence.EntityManager
中的getCriteriaBuilding()
方法來獲取一個javax.persistence.criteria.CriteriaBuilder
實例。
下一步則是獲取javax.persistence.criteria.CriteriaQuery
實例。可以通過javax.persistence.criteria.CriteriaBuilding
中的如下方法達到這一目的:
<T> CriteriaQuery<T> createQuery(Class<T> resultClass)
CriteriaQuery<Tuple> createTupleQuery()
CriteriaQuery<Object> createQuery()
取決於查詢結果的不同預期類型,每個方法的行爲都不相同。
在 《JPA標準說明書》第6章 Criteria API 中已經包含了爲數不少的關於Criteria查詢各方面的參考材料。與其重複這些內容,倒不如來看一些更滿足期待的使用場景。
16.1 擁有類型的Criteria查詢
Criteria查詢的類型(又稱<T>
)描述的是查詢結果的期望類型。它可以是一個實體、一個Integer
或其他任意對象。
16.2 選擇實體
這可能是查詢最常見形式。在應用中選擇實體的實例。
示例 533:選擇根實例
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> criteria = builder.createQuery(Person.class);
Root<Person> root = criteria.from(Person.class);
criteria.select(root);
criteria.where(builder.equal(root.get(Person_.name), "John Doe"));
List<Person> persons = entityManager.createQuery(criteria).getResultList();
示例代碼通過給createQuery()
方法傳遞類引用,指定查詢結果對象應該屬於Person
類。
在示例代碼中,
CriteriaQuery#select
的調用是非必要的,因爲我們只有一個查詢根root
,所以缺省就會選擇root
。
Person_.name
是靜態形式的JPA元模型引用的一個實例。本章我們將只使用這種形式。參考文檔《Hibernate JPA 元模型生成器》瞭解更多關於JPA靜態元模型的信息。
16.3 選擇表達式
選擇表達式的最簡形式是選擇一個實體特定的某個屬性。但也可用於選擇聚合體、算術操作等。
示例534:選擇一個屬性
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<String> criteria = builder.createQuery(String.class);
Root<Person> root = criteria.from(Person.class);
criteria.select(root.get(Person_.nickName));
criteria.where(builder.equal(root.get(Person_.name), "John Doe"));
List<String> nickNames = entityManager.createQuery(criteria).getResultList();
在這個示例中,查詢結果的期望類型是java.lang.String
(Person#nickName
屬性的類型是java.lang.String
)。由於查詢結果可以包含對Person
實體中多個屬性的引用,所以總是需要我們指定要查詢的屬性。這是通過調用Root#get
方法來實現的。
16.4 選擇多個值
事實上通過Criteria查詢多個值有多種實現方式,本文會介紹其中兩種。此外作爲近似替代方案,推薦使用在 《16.6 元組Criteria查詢》 中介紹的元組。或者,考慮試試在 《16.5 選擇包裝對象》 介紹的包裝器查詢。
示例535:選擇一個數組
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object[]> criteria = builder.createQuery(Object[].class);
Root<Person> root = criteria.from(Person.class);
Path<Long> idPath = root.get(Person_.id);
Path<String> nickNamePath = root.get(Person_.nickName);
criteria.select(builder.array(idPath, nickNamePath));
criteria.where(builder.equal(root.get(Person_.name), "John Doe"));
List<Object[]> idAndNickNames = entityManager.createQuery(critria).getResultList();
技術上講,這被歸類爲類型查詢,但從處理結果上可以看到有一些誤導。不論如何,這裏查詢結果的期望類型是一個數組。
示例代碼通過javax.persistence.criteria.CriteriaBuilder
裏的array
方法將獨立選擇顯式組合爲javax.persistence.criteria.CompoundSelection
對象。
示例536:通過multiselect
選擇數組
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object[]> criteria = builder.createQuery(Object[].class);
Root<Person> root = criteria.from(Person.class);
Path<Long> idPath = root.get(Person_.id);
Path<String> nickNamePath = root.get(Person_.nickName);
criteria.multiselect(idPath, nickNamePath);
criteria.where(builder.equal(root.get(Person._name), "John Doe"));
List<Object[]> idAndNickNames = entityManager.createQuery(criteria).getResultList();
和示例535一樣,我們創建了一個返回Object
數組的Criteria類型查詢。這兩個查詢在功能上是等價的。後者使用了multiselect()
方法,其行爲根據構建CriteriaQuery
時傳入的Class
不同而略微不同。在本示例中,它表示選擇並返回一個Object[]
。
16.5 選擇包裝對象
選擇一個“包裝”了多個值的對象,是一種選擇多個值的替代方案。回到上文的示例,與其返回一個[Person#id, Person#nickName]
類型的數組,倒不如聲明一個可以持有這些值的類,並返回該類的對象。
示例537:選擇一個包裝對象
public class PersonWrapper {
private final Long id;
private final String nickName;
public PersonWrapper(Long id, String nickName) {
this.id = id;
this.nickName = nickName;
}
public Long getId() {
return id;
}
public String getNickName() {
return nickName;
}
}
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<PersonWrapper> criteria = builder.createQuery(PersonWrapper.class);
Path<Long> idPath = root.get(Person_.id);
Path<String> nickNamePath = root.get(Person_.nickName);
criteria.select(builder.construct(PersonWrapper.class, idPath, nickNamePath));
criteria.where(builder.equal(root.get(Person_.name)), "John Doe");
List<PersonWrapper> wrappers = entityManager.createQuery(criteria).getResultList();
示例首先定義了一個簡單的包裝類,它會用來包裝結果值。要特別注意一下它的構造器和構造器的參數類型。因爲使用了PersonWrapper
作爲Criteria查詢的類型,所以之後我們會收到PersonWrapper
類型的對象。
這個實例教給我們如何使用javax.persistence.criteria.CriteriaBuilder
中的construct
方法來創建包裝表達式。它會通過對應參數類型的構造函數,將每一行查詢結果作爲參數傳給構造函數以實例化PersonWrapper
對象。之後它便會作爲選擇表達式傳入Criteria查詢。
16.6 元組(Tuple)Criteria查詢
更好的查詢多個值的方法,除了包裝對象(上文剛剛討論過),還有javax.persistence.Tuple
協議。
示例538:選擇一個元組
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteria = builder.createQuery(Tuple.class);
Root<Person> root = criteria.from(Person.class);
Path<Long> idPath = root.get(Person_.id);
Path<String> nickNamePath = root.get(Person_.nickName);
criteria.multselect(idPath, nickNamePath);
criteria.where(builder.equal(root.get(Person_.name), "John Doe"));
List<Tuple> tuples = entityManager.createQuery(criteria).getResultList();
for (Tuple tuple: tuples) {
Long id = tuple.get(idPath);
String nickName = tuple.get(nickNamePath);
}
// 也可以使用索引
for (Tuple tuple: tuples) {
Long id = (Long) tuple.get(0);
String nickName = (String) tuple.get(1);
}
這個示例展示瞭如何通過javax.persistence.Tuple
接口訪問查詢結果。在這裏我們使用的是javax.persistence.criteria.CriteriaBuilder
下的createQuery(Tuple.class)
方法,此外也可以顯式地調用createTupleQuery()
。
我們又見到multiselect()
了,上次是在示例536中。不同之處是這次查詢的類型是javax.persistence.Tuple
,故這次複合查詢的結果會解析成元組對象。
javax.persistence.Tuple
協議提供了元組元素的三種訪問方式:
- 類型式
示例538展示了這種方式,通過調用tuple.get(idPath)
和tuple.get(nickNamePath)
來訪問元素。本方式基於構建查詢時使用的javax.persistence.TupleElement
表達式對元組裏的值進行類型化訪問。 - 位置式
基於位置訪問元組內的值。簡單形式Object get(int position)
和示例535、示例536中的寫法很像。<X> X get(int position, Class<X> type)
可以實現基於位置的類型化訪問,但前提是參數聲明的類型必須契合元組值的類型。 - 別名式
通過(可選的)別名聲明訪問元組下的值。示例代碼中的查詢並沒有指定別名。別名的指定是通過javax.persistence.criteria.Selection
下的alias
方法實現的。和位置式訪問類似,別名式訪問也包含無類型的Object get(String alias)
和類型化的<X> X get(String alias, Class<X> type)
兩種訪問方式。
16.7 FROM子句
CriteriaQuery
對象定義了基於一個或多個實體、嵌入體或基本抽象元類型的查詢。查詢的根對象,是可以訪問到其他類型對象的一個或多個實體對象。—— JPA 標準說明書(P262), 6.5.2 查詢的根
FROM子句中所有的獨立部分(根、連接、路徑)均實現了
javax.persistence.criteria.From
接口。
16.8 根(Root)
根定義了在查詢中所有可用的連接、路徑和屬性的基本信息。根總是實體類型的。Criteria查詢中根的定義和添加是通過javax.persistence.criteria.CriteriaQuery
裏的重載方法實現的。
示例539:根的方法
<X> Root<X> from(Class<X> type);
<X> Root<X> from (EntityType<X> entityType);
示例540:添加一個根
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> criteria = builder.createQuery(Person.class);
Root<Person> root = criteria.from(Person.class);
Criteria查詢可能會定義多個根,其結果是會創建新增根與其他根之間的笛卡爾積。下文是定義Person
和Partner
實體間笛卡爾積的示例:
示例541:添加多個根
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteria = builder.createQuery(Tuple.class);
Root<Person> personRoot = criteria.from(Person.class);
Root<Partner> partnerRoot = criteria.from(partner.class);
criteria.multiselect(personRoot, partnerRoot);
Predicate personRestriction = builder.and(
builder.equal(personRoot.get(Person_.address), address),
builder.isNotEmpty(personRoot.get(Person_phones))
);
Predicate partnerRestriction = builder.and(
builder.like(partnerRoot.get(Partner_.name), prefix),
builder.equal(partnerRoot.get(Partner_.version), 0)
);
criteria.where(builder.and(personRestriction, partnerRestriction));
List<Tuple> tuples = entityManager.createQuery(criteria).getResultList();
16.9 連接(Join)
連接可以實現對其他javax.persistence.criteria.From
聯合屬性或嵌入式屬性的訪問。連接可以通過javax.persistence.criteria.From
接口中一系列join
方法的重載創建。
示例542:連接示例
CriteriaBuilder builder = entityManager.getCriteriaBulder();
CriteriaQuery<Phone> criteria = builder.createQuery(Phone.class);
Root<Phone> root = criteria.from(Phone.class);
// Phone.person是一個@ManyToOne(多對一)
Join<Phone, Person> personJoin = root.join(Phone._person);
// Person.addresses是一個@ElementCollection(元素集合)
Join<Person, String> addressesJoin = personJoin.join(Person._addresses);
criteria.where(builder.isNotEmpty(root.get(Phone_.calls)));
List<Phone> phones = entityManager.createQuery(criteria).getResultList();
16.10 拉取(Fetch)
和HQL、JPQL類似,Criteria查詢也可以指定拉取擁有者的關聯數據。拉取可以通過javax.persistence.criteria.From
接口中的一系列fetch
重載方法創建。
示例543:連接拉取示例
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Phone> criteria = builder.createQuery(Phone.class);
Root<Phone> root = criteria.from(Phone.class);
// Phone.person是一個@ManyToOne
Fetch<Phone, Person> personFetch = root.fetch(Phone_.person);
// Person.addresses是一個@ElementCollection
Fetch<Person, String> addressesJoin = personFetch.fetch(Person_.addresses);
criteria.where(builder.isNotEmpty(root.get(Phone_.calls)));
List<Phone> phones = entityManager.createQuery(criteria).getResultList();
從技術上講,嵌入式屬性總會會跟隨其擁有者被拉取。然而,因爲默認的集合元素是
LAZY
的,所以我們需要通過javax.persistence.criteria.Fetch
對象來拉取Phone#addresses
。
16.11 路徑表達式
根、連接和拉取本身都是路徑。
16.12 使用參數
示例544:參數示例
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> criteria = builder.createQuery(Person.class);
Root<Person> root = criteria.from(Person.class);
ParameterExpression<String> nickNameParameter = builder.parameter(String.class);
criteria.where(builder.equal(root.get(Person_.nickName), nickNameParameter));
TypedQuery<Person> query = entityManager.createQuery(criteria);
query.setParameter(nickNameParameter, "JD");
List<Person> persons = query.getResultList();
使用javax.persistence.criteria.CriteriaBuilder
中的parameter
方法可以獲取參數對象的引用。之後可以通過這個引用給javax.persistence.Query
綁定參數值。
16.13 group by 的使用
注:這裏原文沒有使用之前一直使用的基於JPA靜態元模型獲取root的模式,推測應該是文檔這一小節一直沒有更新。
示例545:分組查詢示例
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteria = builder.createQuery(Tuple.class);
Root<Person> root = criteria.from(Person.class);
criteria.groupBy(root.get("address"));
criteria.multiselect(root.get("address"), builder.count(root));
List<Tuple> tuples = entityManager.createQuery(criteria).getResultList();
for (Tuple tuple : tuples) {
String name = (String) tuple.get(0);
Long count = (Long) tuple.get(1);
}