JPA 2.0动态查询机制Criteria API怎么用

发布时间:2021-12-27 09:35:06 作者:小新
来源:亿速云 阅读:154

小编给大家分享一下JPA 2.0动态查询机制Criteria API怎么用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!

自从 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 使用,比如 EntityManagerFactoryEntityManager

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 查询字符串的语法有误。清单 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 的核心构造及其基本使用:

现在归纳 清单 2 中的简单例子的基本方面:

持久化域的元模型

讨论 清单 2 时指出了一个不常见的构造:Person_.age,它表示 Person 的持久化属性 age清单 2 使用 Person_.age 形成一个路径表达式,它通过 p.get(Person_.age)Root< Person> 表达式 p 导航而来。Person_.agePerson_ 类中的公共静态字段,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;
@Entitypublic 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 的类型可以确定 Personage 属性是一个数字字段,不能与 String 进行比较。

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

Metamodel API

我将一个元模型类定义为一个持久化实体类的描述。就像 Reflection API 需要其他接口(比如 java.lang.reflect.Fieldjava.lang.reflect.Method )来描述 java.lang.Class 的组成一样,JPA Metamodel API 也需要其他接口(比如 SingularAttributePluralAttribute)来描述元模型类的类型及其属性。

图 3 显示了在 Metamodel API 中定义用于描述类型的接口:


图 3. Metamodel API 中的持久化类型的接口的层次结构
JPA 2.0动态查询机制Criteria API怎么用

图 4 显示了在 Metamodel API 中定义用于描述属性的接口:


图 4. Metamodel API 中的持久化属性的接口的层次结构
JPA 2.0动态查询机制Criteria API怎么用

JPA 的 Metamodel API 接口比 Java Reflection API 更加专业化。需要更细微的差别来表达关于持久化的丰富元信息。例如,Java Reflection API 将所有 Java 类型表示为 java.lang.Class。即没有通过独立的定义对概念进行区分,比如类、抽象类和接口。当然,您可以询问 Class 它是一个接口还是一个抽象类,但这与通过两个独立的定义表示接口和抽象类的差别不同。

Java Reflection API 在 Java 语言诞生时就被引入(对于一种常见的多用途编程语言而言,这曾经是一个非常前沿的概念),但是经过多年的发展才认识到强类型系统的用途和强大之处。JPA Metamodel API 将强类型引入到持久化实体中。例如,持久化实体在语义上区分为 MappedSuperClassEntityEmbeddable。在 JPA 2.0 之前,这种语义区分是通过持久化类定义中的对应类级别注释来表示的。JPA Metamodel 在 javax.persistence.metamodel 包中描述了 3 个独立的接口( MappedSuperclassTypeEntityTypeEmbeddableType ),以更加鲜明的对比它们的语义特征。类似地,可以通过接口(比如 SingularAttributeCollectionAttributeMapAttribute)在类型定义级别上区分持久化属性。

除了方便描述之外,这些专门化的元模型接口还有实用优势,能够帮助构建类型安全的查询从而减少运行时错误。您在前面的例子中看到了一部分优势,随着我通过 CriteriaQuery 描述关于连接的例子,您将看到更多优势。

运行时作用域

一般而言,可以将 Java Reflection API 的传统接口与专门用于描述持久化元数据的 javax.persistence.metamodel 的接口进行比较。要进一步进行类比,则需要对元模型接口使用等效的运行时作用域概念。java.lang.Class 实例的作用域由 java.lang.ClassLoader 在运行时划分。一组相互引用的 Java 类实例必须在 ClassLoader 作用域下定义。作用域的边界是严格封闭 的,如果在 ClassLoader L 作用域下定义的类 A 试图引用不在 ClassLoader L 作用域之内的类 B,结果将收到可怕的 ClassNotFoundExceptionNoClassDef FoundError(对于处理包含多个 ClassLoader 的环境的开发人员或部署人员而言,问题就复杂了)。

现在将一组严格的可相互引用的类称为运行时作用域,而在 JPA 1.0 中称为持久化单元。持久化单元作用域的持久化实体在 META-INF/persistence.xml 文件的 < class> 子句中枚举。在 JPA 2.0 中,通过 javax.persistence.metamodel.Metamodel 接口让开发人员可以在运行时使用作用域。Metamodel 接口是特定持久化单元知道的所有持久化实体的容器,如图 5 所示:


图 5. 元模型接口是持久化单元中的类型的容器
JPA 2.0动态查询机制Criteria API怎么用

这个接口允许通过元模型元素的对应持久化实体类访问元模型元素。例如,要获得对 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 提供者的责任,但非规范元模型则不存在这一要求。使用非规范元模型的应用程序必须依赖于特定供应商机制,或开发自己的机制来在运行时初始化元模型属性的字段值。

JPA 2.0动态查询机制Criteria API怎么用 

注释处理和元模型生成

如果您有许多持久化实体,您将倾向于不亲自编写元模型类,这是很自然的事情。持久化提供者应该 为您生成这些元模型类。在规范中没有强制规定这种工具或生成机制,但是 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() 子句。

JPA 2.0动态查询机制Criteria 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 定义 CollectionJoinListJoinMapJoin。(图 1 显示了各种连接表达式)。在 清单 8 的第 3 行不需要进行显式的转换,因为 CriteriaQuery 和 Metamodel API 通过覆盖 join() 的方法能够识别和区分声明为 java.util.CollectionList 或者 SetMap 的属性类型。

在查询中使用连接在连接实体上形成一个谓词。因此,如果您想要选择有一个或多个未发送 PurchaseOrderCustomer,可以通过状态属性从连接表达式 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 — 一个非持久化实体 — 有一个接受 Stringint 参数的构造方法,那么 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);
JPA 2.0动态查询机制Criteria API怎么用 

这个查询返回一个结果列表,它的每个元素都是一个 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));

因为查询结果类型为 CustomerDetailsmultiselect() 将其预测条件解释为 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 个例子:

弱类型和动态查询构建

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 editingList< 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();
JPA 2.0动态查询机制Criteria API怎么用 

CriteriaQuery 上的 setter 方法 — select()where()orderBy() — 使用新的参数替换先前的值。对应的 getter 方法(比如 getOrderList())返回的列表不是活动的,即在返回列表上添加或删除元素不会导致修改 CriteriaQuery;另外,一些供应商甚至返回不可变的列表以阻止意外使用。因此,良好的实践是在添加和删除新的表达式之前,将返回列表复制到一个新的列表中。

根据例子进行查询

动态查询 API 中的另一个有用特性就是它能够轻松地支持根据例子进行查询。根据例子进行查询(由 IBM&reg; 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 评级为 1Employee。要进一步控制比较,可以指定不用于比较的可选属性,以及为值为 String 的属性指定比较方式。

以上是“JPA 2.0动态查询机制Criteria API怎么用”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注亿速云行业资讯频道!

推荐阅读:
  1. Spring+JPA如何集成Hibernate
  2. Spring Data JPA 简单查询

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

上一篇:Redis快速部署为Docker容器的实现方法是什么

下一篇:register_backward_hook(hook)和register_forward_hook(hook)怎么使用

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》