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);
}