编写类型安全的查询
到目前为止,我已经构建了 CriteriaQuery的组件和相关的元模型类。现在,我将展示如何使用 Criteria API 开发一些查询。
函数表达式
函数表达式将一个函数应用到一个或多个输入参数以创建新的表达式。函数表达式的类型取决于函数的性质及其参数的类型。输入参数本身可以是表达式或文本值。编译器的类型检查规则与 API 签名结合确定什么是合法输入。
考虑一个对输入表达式应用平均值的单参数表达式。CriteriaQuery选择所有 Account的平均余额,如清单 5 所示:
清单 5. CriteriaQuery中的函数表达式
CriteriaQuery c = cb.createQuery(Double.class);
Root 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()子句。
Fluent API
如这个例子所示,Criteria API 方法经常返回可以直接在相关方法中使用的类型,从而提供了一种称为 Fluent API 的流行编程风格。
该查询表达式是一个构建块,可以通过组装它为查询定义最后的选择谓词。清单 6中的例子显示了通过导航到 Account的余额创建的 Path表达式,然后 Path表达式被用作两个二进制函数表达式(greaterThan()和 lessThan())的输入表达式,这两个表达式的结果都是一个布尔表达式或一个谓词。然后,通过 and()操作合并谓词以形成最终的选择谓词,查询的 where()子句将计算该谓词:
清单 6. CriteriaQuery中的 where()谓词
CriteriaQuery c = cb.createQuery(Account.class);
Root account = c.from(Account.class);
Path[I] 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 c = cb.createQuery(Account.class);
Root account = c.from(Account.class);
Path owner = account.get(Account_.owner);
Path name = owner.get(Person_.name);
c.where(cb.in(name).value("X").value("Y").value("Z"));
这个例子通过两个步骤从 Account进行导航,创建一个表示帐户所有者的名称的路径。然后,它创建一个使用路径表达式作为输入的 in()表达式。in()表达式计算它的输入表达式是否等于它的参数之一。这些参数通过 value()方法在 In表达式上指定,In的签名如下所示:
In value(T value);
注意如何使用 Java 泛型指定仅对值的类型为 T的成员计算 In表达式。因为表示 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的持久化属性,如清单 8 所示:
清单 8. 连接多值属性
CriteriaQuery q = cb.createQuery(Customer.class);
Root c = q.from(Customer.class);
SetJoin o = c.join(Customer_.orders);
连接表达式从根表达式 c创建,持久化属性 Customer.orders由连接源(Customer)和 Customer.orders属性的可绑定类型进行参数化,可绑定类型是 PurchaseOrder而 不是已声明的类型 java.util.Set。此外还要注意,因为初始属性的类型为 java.util.Set,所以生成的连接表达式为 SetJoin,它是专门针对类型被声明为 java.util.Set的属性的 Join。类似地,对于其他受支持的多值持久化属性类型,该 API 定义 CollectionJoin、ListJoin和 MapJoin。(图 1显示了各种连接表达式)。在 清单 8的第 3 行不需要进行显式的转换,因为 CriteriaQuery和 Metamodel API 通过覆盖 join()的方法能够识别和区分声明为 java.util.Collection或 List或者 Set或 Map的属性类型。
在查询中使用连接在连接实体上形成一个谓词。因此,如果您想要选择有一个或多个未发送 PurchaseOrder的 Customer,可以通过状态属性从连接表达式 o进行导航,然后将其与 DELIVERED状态比较,并否定谓词:
Predicatep = cb.equal(o.get(PurchaseOrder_.status), Status.DELIVERED)
.negate();
创建连接表达式需要注意的一个地方是,每次连接一个表达式时,都会返回一个新的表达式,如清单 9 所示:
清单 9. 每次连接创建一个唯一的实例
SetJoin o1 = c.join(Customer_.orders);
SetJoin o2 = c.join(Customer_.orders);
asserto1 == 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";
参数化查询在查询执行之前绑定参数的值:
Queryquery = em.createQuery(jpql).setParameter("age", 20);
List result = query.getResultList();
在 JPQL 查询中,查询字符串中的参数以命名方式(前面带有冒号,例如 :age)或位置方式(前面带有问号,例如 ?3)编码。在 CriteriaQuery中,参数本身就是查询表达式。与其他表达式一样,它们是强类型的,并且由表达式工厂(即 QueryBuilder)构造。然后,可以参数化 清单 2中的查询,如清单 10 所示:
清单 10. 在 CriteriaQuery中使用参数
ParameterExpression[I] age = qb.parameter(Integer.class);
Predicatecondition = qb.gt(p.get(Person_.age), age);
c.where(condition);
TypedQuery q = em.createQuery(c);
List result = q.setParameter(age, 20).getResultList();
比较该参数使用和 JPQL 中的参数使用:参数表达式被创建为带有显式类型信息 Integer,并且被直接用于将值 20绑定到可执行查询。额外的类型信息对减少运行时错误十分有用,因为阻止参数与包含不兼容类型的表达式比较,或阻止参数与不兼容类型的值绑定。JPQL 查询的参数不能提供任何编译时安全。
清单 10中的例子显示了一个直接用于绑定的未命名表达式。还可以在构造参数期间为参数分配第二个名称。对于这种情况,您可以使用这个名称将参数值绑定到查询。不过,您不可以使用位置参数。线性 JPQL 查询字符串中的整数位置有一定的意义,但是不能在概念模型为查询表达式树的 CriteriaQuery上下文中使用整数位置。
JPA 查询参数的另一个有趣方面是它们没有内部值。值绑定到可执行查询上下文中的参数。因此,可以合法地从相同的 CriteriaQuery创建两个独立可执行的查询,并为这些可执行查询的相同参数绑定两个整数值。
预测结果
您已经看到 CriteriaQuery在执行时返回的结果已经在 QueryBuilder构造 CriteriaQuery时指定。查询的结果被指定为一个或多个 预测条件。可以通过两种方式之一在 CriteriaQuery接口上指定预测条件:
CriteriaQuery select(Selection selection);
CriteriaQuery multiselect(Selection... selections);
最简单并且最常用的预测条件是查询候选类。它可以是隐式的,如清单 11 所示:
清单 11. CriteriaQuery默认选择的候选区段
CriteriaQuery q = cb.createQuery(Account.class);
Root account = q.from(Account.class);
List accounts = em.createQuery(q).getResultList();
在 清单 11中,来自 Account的查询没有显式地指定它的选择条件,并且和显式地选择的候选类一样。清单 12 显示了一个使用显式选择条件的查询:
清单 12. 使用单个显式选择条件的 CriteriaQuery
CriteriaQuery q = cb.createQuery(Account.class);
Root account = q.from(Account.class);
q.select(account);
List accounts = em.createQuery(q).getResultList();
如果查询的预测结果不是候选持久化实体本身,那么可以通过其他几个构造方法来生成查询的结果。这些构造方法包含在 QueryBuilder接口中,如清单 13 所示:
清单 13. 生成查询结果的方法
CompoundSelection construct(Class result, Selection... terms);
CompoundSelection array(Selection... terms);
CompoundSelection tuple(Selection... terms);
清单 13中的方法构建了一个由其他几个可选择的表达式组成的预测条件。construct()方法创建给定类参数的一个实例,并使用来自输入选择条件的值调用一个构造函数。例如,如果 CustomerDetails —一个非持久化实体 —有一个接受 String和 int参数的构造方法,那么 CriteriaQuery可以通过从选择的 Customer —一个持久化实体 —实例的名称和年龄创建实例,从而返回 CustomerDetails作为它的结果,如清单 14 所示:
清单 14. 通过 construct()将查询结果包放入类的实例
CriteriaQuery q = cb.createQuery(CustomerDetails.class);
Root 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 q = cb.createQuery(Object[].class);
Root c = q.from(Customer.class);
q.select(cb.array(c.get(Customer_.name), c.get(Customer_.age));
List 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 q = cb.createTupleQuery();
Root c = q.from(Customer.class);
TupleElement tname = c.get(Customer_.name).alias("name");
q.select(cb.tuple(tname, c.get(Customer_.age).alias("age");
List result = em.createQuery(q).getResultList();
String name = result.get(0).get(name);
String age= result.get(0).get(1);
嵌套限制
从理论上讲,可以通过嵌套 Tuple等条件(它的元素本身为 Object[]或 Tuple)来构成复杂的结果。不过,JPA 2.0 规范禁止此类嵌套。multiselect()的输入条件不能是数组或值为二元组的复合条件。允许作为 multiselect()参数的唯一复合条件由 construct()方法创建(该方法仅表示一个元素)。
不过,OpenJPA 没有限制在一个复合选择条件中嵌套其他的复合选择条件。
这个查询返回一个结果列表,它的每个元素都是一个 Tuple。反过来,每个二元组都带有两个元素 —可以被每个 TupleElement的索引或别名(如果有的话)访问,或直接被 TupleElement访问。清单 16中需要注意的两点是 alias()的使用,它是将一个名称绑定到查询表达式的一种方式(创建一个新的副本),和 QueryBuilder上的 createTupleQuery()方法,它仅是 createQuery(Tuple.class)的代替物。
这些能够改变结果的方法的行为和在构造期间被指定为 CriteriaQuery的类型参数结果共同组成 multiselect()方法的语义。这个方法根据最终实现结果的 CriteriaQuery的结果类型解释它的输入条件。要像 清单 14一样使用 multiselect()构造 CustomerDetails实例,您需要将 CriteriaQuery的类型指定为 CustomerDetails,然后使用将组成 CustomerDetails构造方法的条件调用 multiselect(),如清单 17 所示:
清单 17. 基于结果类型的 multiselect()解释条件
CriteriaQuery q = cb.createQuery(CustomerDetails.class);
Root c = q.from(Customer.class);
q.multiselect(c.get(Customer_.name), c.get(Customer_.age));
因为查询结果类型为 CustomerDetails,multiselect()将其预测条件解释为 CustomerDetails构造方法参数。如将查询指定为返回 Tuple,那么带有相同参数的 multiselect()方法将创建 Tuple实例,如清单 18 所示:
清单 18. 使用 multiselect()方法创建 Tuple实例
CriteriaQuery q = cb.createTupleQuery();
Root c = q.from(Customer.class);
q.multiselect(c.get(Customer_.name), c.get(Customer_.age));
如果以 Object作为结果类型或没有指定类型参数时,multiselect()的行为会变得更加有趣。在这些情况中,如果 multiselect()使用单个输入条件,那么返回值将为所选择的条件。但是如果 multiselect()包含多个输入条件,结果将得到一个 Object[]。
|