持续学习Jpa 2.0 Criteria

序言

这几天研究了下JPA的标准查询,名为:JPA criteria查询.相比JPQL,其优势是类型安全,更加的面向对象.

使用标准查询,开发人员可在编译的时候就检查查询的正确与否。

更详细的,可以参考Jpa 2.0规范-动态查询Criteria Api

JPA元模型概念,及使用

在JPA中,标准查询是以元模型的概念为基础的.元模型是为具体持久化单元的受管实体定义的.这些实体可以是实体类,嵌入类或者映射的父类.提供受管实体元信息的类就是元模型类.

描述受管类的状态和他们之间的关系的静态元模型类可以

  • 1.从注解处理器产生
  • 2.从程序产生
  • 3.用EntityManager访问.

如下code,一个简单的实体类package com.demo.entities;下,实体类Employee ,假设该实体有诸如id,name和age的基本属性,还有与类Address的OneToMany关联:

@Entity
@Table
public class Employee{ 
    private int id;  

    private String name;

    private int age;

    @OneToMany
    private List<Address> addresses;
    // Other code…

}

实体类的标准元模型类的名字将是使用 javax.persistence.StaticMetamodel注解的Employee_。元模型类的属性全部是static和public的。Employee的每一个属性都会使用在JPA2规范中描述的以下规则在相应的元模型类中映射:

  • 诸如id,name和age的非集合类型,会定义静态属性SingularAttribute b,这里b是定义在类A中的类型为B的一个对象。
  • 对于Addess这样的集合类型,会定义静态属性ListAttribute b,这里List对象b是定义在类A中类型B的对象。其它集合类型可以是SetAttribute, MapAttribute 或 CollectionAttribute 类型。 以下是用注解处理器产生的元模型类package com.demo.entities;下:
import javax.annotation.Generated;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.ListAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@Generated("org.hibernate.jpamodelgen.JPAMetaModelEntityProcesso")
@StaticMetamodel(Employee.class)
public class Employee_ {    

    public static volatile SingularAttribute<Employee, Integer> id;  

    public static volatile SingularAttribute<Employee, Integer> age;  

    public static volatile SingularAttribute<Employee, String> name;   

    public static volatile ListAttribute<Employee, Address> addresses;

}

就像它的名字表明的,注解处理器处理注解,帮助产生源代码。注解处理在编译时就能激活。元模型类遵循JPA2.0规范中为定义标准元模型类而描述的规则创建。

使用元模型类最大的优势是凭借其实例化可以在编译时访问实体的持久属性.该特性使得criteria 查询更加类型安全.

元模型API与Java中的标准反射API密切相关。主要不同在于使用标准反射API编译器无法验证其正确性。例如:下面的代码会通过编译测试:

Class myClass = Class.forName("com.demo.Test");

Field myField = myClass.getField("myName");

编译器假定com.demo.Test中定义了属性myName,一旦该类并没有定义属性myName,编译器将抛出运行时异常。

元模型API会强制编译器检查适当的值是否分配给实体类的持久属性。例如:考虑Employee类的age属性,它是Integer变量。若该属性被赋值为String类型的值,编译器会抛出错误。该实现并不要求支持非标准特性。程序员编写的元模型类通常称为非标准元模型类。当EntityManagerFactory 创建时,持久化提供者会初始化元模型类的属性。

使用criteria 查询

为了更好的理解criteria 查询,考虑拥有Employee实例集合的Dept实体,Employee和Dept的元模型类的代码如下:

//All Necessary Imports
@StaticMetamodel(Dept.class)
public class Dept_ {   

    public static volatile SingularAttribute<Dept, Integer> id;  

    public static volatile ListAttribute<Dept, Employee> employeeCollection;   

    public static volatile SingularAttribute<Dept, String> name;

}

//All Necessary Imports
@StaticMetamodel(Employee.class)
public class Employee_ {    

    public static volatile SingularAttribute<Employee, Integer> id;   

    public static volatile SingularAttribute<Employee, Integer> age;   

    public static volatile SingularAttribute<Employee, String> name;   

    public static volatile SingularAttribute<Employee, Dept> deptId;

}

下面的代码片段展示了一个criteria 查询,它用于获取所有年龄大于24岁的员工:

CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);

Root<Employee> employee = criteriaQuery.from(Employee.class);

Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24);

criteriaQuery.where(condition);

TypedQuery<Employee> typedQuery = em.createQuery(criteriaQuery);

List<Employee> result = typedQuery.getResultList();

对应的SQL: SELECT * FROM employee WHERE age > 24

构建CriteriaQuery 实例API说明

CroteriaQuery

CriteriaQuery对象必须在实体类型或嵌入式类型上的Criteria 查询上起作用。
它通过调用 CriteriaBuilder, createQuery 或CriteriaBuilder.createTupleQuery 获得。
CriteriaBuilder就像CriteriaQuery 的工厂一样。
CriteriaBuilder工厂类是调用EntityManager.getCriteriaBuilder 或 EntityManagerFactory.getCriteriaBuilder而得。
Employee实体的 CriteriaQuery 对象以下面的方式创建:

CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);

QueryRoot

AbstractQuery是CriteriaQuery 接口的父类。它提供得到查询根的方法。
Criteria查询的查询根定义了实体类型,能为将来导航获得想要的结果,它与SQL查询中的FROM子句类似。
Root实例也是类型化的,且定义了查询的FROM子句中能够出现的类型。
查询根实例能通过传入一个实体类型给 AbstractQuery.from方法获得。
Criteria查询,可以有多个查询根。
Employee实体的查询根对象可以用以下的语法获得 :

Root<Employee> employee = criteriaQuery.from(Employee.class);

过滤Queries

过滤条件应用到SQL语句的FROM子句中。
在criteria 查询中,查询条件通过Predicate 或Expression 实例应用到CriteriaQuery 对象上。
这些条件使用 CriteriaQuery .where 方法应用到CriteriaQuery 对象上。
CriteriaBuilder 也是作为Predicate 实例的工厂,Predicate 对象通过调用CriteriaBuilder 的条件方法( equal,notEqual, gt, ge,lt, le,between,like等)创建。
Predicate 实例也可以用Expression 实例的 isNull, isNotNull 和 in方法获得,复合的Predicate 语句可以使用CriteriaBuilder的and, or andnot 方法构建。
下面的代码片段展示了Predicate 实例检查年龄大于24岁的员工实例:

Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24);

criteriaQuery.where(condition);

Employee_元模型类age属性,称之为路径表达式。若age属性与String文本比较,编译器会抛出错误,这在JPQL中是不可能的。

执行查询与获取元模型实例

当EntityManager.createQuery(CriteriaQuery)方法调用时,一个可执行的查询实例会创建,该方法返回指定从 criteria 查询返回的实际类型的TypedQuery 对象。

TypedQuery 接口是javax.persistence.Queryinterface.的子类型。

在该片段中, TypedQuery 中指定的类型信息是Employee,调用getResultList时,查询就会得到执行

TypedQuery<Employee> typedQuery = em.createQuery(criteriaQuery);

List<Employee> result = typedQuery.getResultList();

元模型实例通过调用 EntityManager.getMetamodel 方法获得,

EntityType由元模型实例通过调用Metamodel.entity(Employee.class)而获得,其被传入 CriteriaQuery.from 获得查询根。

Metamodel metamodel = em.getMetamodel();

EntityType<Employee> Employee_ = metamodel.entity(Employee.class);

Root<Employee> empRoot = criteriaQuery.from(Employee_);

也可由调用Root.getModel方法获得元模型信息。

类型 EntityType的实例Dept_和name属性可以调用getSingularAttribute 方法获得,它与String文本进行比较:

CriteriaQuery criteriaQuery = criteriaBuilder.createQuery();

Root<Dept> dept = criteriaQuery.from(Dept.class);

EntityType<Dept> Dept_ = dept.getModel();

Predicate testCondition = criteriaBuilder.equal(dept.get(Dept_.getSingularAttribute("name", String.class)), "Ecomm");

Expression

Expression对象用在查询语句的select,where和having子句中,该接口有 isNull, isNotNull 和 in方法,下面的代码片段展示了Expression.in的用法,employee的年龄限制为20或24。

CriteriaQuery<Employee> criteriaQuery = criteriaBuilder .createQuery(Employee.class);

Root<Employee> employee = criteriaQuery.from(Employee.class);

criteriaQuery.where(employee.get(Employee_.age).in(20, 24));

em.createQuery(criteriaQuery).getResultList();

对应的 SQL: SELECT * FROM employee WHERE age in (20, 24)

复合谓词

Criteria Query也允许开发者编写复合谓词,通过该查询可以为多条件测试下面的查询检查两个条件。首先,name属性是否以M开头,其次,employee的age属性是否是25。逻辑操作符and执行获得结果记录。

and、or

criteriaQuery.where(criteriaBuilder.and(criteriaBuilder.like(employee.get(Employee_.name), "M%"), criteriaBuilder.equal(employee.get(Employee_.age), 25)));

em.createQuery(criteriaQuery).getResultList();

连接查询

在SQL中,连接跨多张表以获取查询结果,类似的实体连接通过调用 From.join 执行,连接帮助从一个实体导航到另一个实体以获得查询结果。
Root的join方法返回一个 Join类型(也可以是SetJoin,,ListJoin,MapJoin 或者 CollectionJoin类型)。

默认情况下,连接操作使用内连接,而外连接可以通过在join方法中指定JoinType参数为LEFT或RIGHT来实现。

CriteriaQuery<Dept> cqDept = criteriaBuilder.createQuery(Dept.class);

Root<Dept> deptRoot = cqDept.from(Dept.class);

Join<Dept, Employee> employeeJoin = deptRoot.join(Dept_.employeeCollection);

cqDept.where(criteriaBuilder.equal(employeeJoin.get(Employee_.deptId).get(Dept_.id), 1));

TypedQuery<Dept> resultDept = em.createQuery(cqDept);

需注意,默认情况下,每次连接都会创建一个实例(即便是同一张表,多个c.join会创建多个left join)。故需特殊处理

抓取连接

当涉及到collection属性时,抓取连接对优化数据访问是非常有帮助的。这是通过预抓取关联对象和减少懒加载开销而达到的。
使用 criteria 查询,fetch方法用于指定关联属性
Fetch连接的语义与Join是一样的,因为Fetch操作不返回Path对象,所以它不能将来在查询中引用。
在以下例子中,查询Dept对象时employeeCollection对象被加载。若未配置fetch,就会有第二次查询数据库,因为有懒加载。

CriteriaQuery<Dept> d = cb.createQuery(Dept.class);

Root<Dept> deptRoot = d.from(Dept.class);

deptRoot.fetch("employeeCollection", JoinType.LEFT);

d.select(deptRoot);

List<Dept> dList = em.createQuery(d).getResultList();

对应SQL: SELECT * FROM dept d, employee e WHERE d.id = e.deptId

函数表达式

可以使用通用的函数。支持的函数可自行查看

CriteriaQuery<Integer> d = cb.createQuery(Integer.class);

Root<Employee> employeeRoot = d.from(Employee.class);

d.select(cb.avg(employeeRoot.get(Employee_.age)));

对应SQL: SELECT avg(e.age) from employee e

弱类型和动态查询构建

Criteria API 的强类型检查基于开放期间的实例化元模型类的可用性。不过,在某些情况下,选择的实体仅能够在运行时决定。为了支持这种用法,Criteria API 方法提供一个并列版本,其中持久化属性通过它们的名称进行引用(类似于 Java Reflection API),而不是引用实例化静态元模型属性。该 API 的这个并列版本可以通过牺牲编译时类型检查来真正地支持动态查询构造。

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.get("balance");

c.where(cb.and(cb.greaterThan(balance, 100), cb.lessThan(balance), 200)));

不过,弱类型 API 不能够返回正确的泛型表达式,因此生成一个编辑器来警告未检查的转换。一种消除这些烦人的警告消息的方法是使用 Java 泛型不常用的工具:参数化方法调用,比如代码中通过调用 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() 函数,如下所示:

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

可编辑查询

可以以编程的方式编辑 CriteriaQuery。可以改变查询的子句,比如它的选择条件、WHERE 子句中的选择谓词和 ORDER BY 子句中的排序条件。可以在典型的 “在结果中搜索” 工具中使用这个编辑功能,以添加更多限制在后续步骤中进一步细化查询谓词。

如下例子创建了一个根据名称对结果进行排序的查询,然后编辑该查询以根据邮政编码进行查询:

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

根据例子进行查询

动态查询 API 中的另一个有用特性就是它能够轻松地支持根据例子进行查询。根据例子进行查询(由 IBM® Research 在 1970 年开发出来)通常被作为早期的软件终端用户可用性例子引用。根据例子进行查询的理念使用模板实例,而不是为查询指定精确的谓词。有了给定的模板实例之后,将创建一个联合谓词,其中每个谓词都是模板实例的非 null 和非默认属性值。执行该查询将计算谓词以查找所有与模板实例匹配的实例。根据例子进行查询曾考虑添加到 JPA 2.0 中,但最终没有添加。OpenJPA 通过它的扩展 OpenJPAQueryBuilder 接口支持这种查询

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 的属性指定比较方式。

路径表达式

Root实例,Join实例或者从另一个Path对象的get方法获得的对象使用get方法可以得到Path对象,当查询需要导航到实体的属性时,路径表达式是必要的。
Get方法接收的参数是在实体元模型类中指定的属性。
Path对象一般用于Criteria查询对象的select或where方法。

Path,表示从 Root 表达式导航到的持久化属性。Root 是一个没有父类的特殊 Path

例子如下:这里返回结果只有Dept的name属性

CriteriaQuery<String> criteriaQuery = criteriaBuilder.createQuery(String.class);

Root<Dept> root = criteriaQuery.from(Dept.class);

criteriaQuery.select(root.get(Dept_.name));

参数化表达式

在JPQL中,查询参数是在运行时通过使用命名参数语法(冒号加变量,如 :age)传入的。在Criteria查询中,查询参数是在运行时创建ParameterExpression对象并为在查询前调用TypeQuery,setParameter方法设置而传入的。

下面代码片段展示了类型为Integer的ParameterExpression age,它被设置为24:

ParameterExpression<Integer> age = criteriaBuilder.parameter(Integer.class);

Predicate condition = criteriaBuilder.gt(testEmp.get(Employee_.age), age);

criteriaQuery.where(condition);

TypedQuery<Employee> testQuery = em.createQuery(criteriaQuery);

List<Employee> result = testQuery.setParameter(age, 24).getResultList();

对应SQL: SELECT * FROM Employee WHERE age = 24;

排序结果

Criteria查询的结果能调用CriteriaQuery.orderBy方法排序,该方法接收一个Order对象做为参数。通过调用 CriteriaBuilder.asc 或 CriteriaBuilder.Desc,Order对象能被创建。

以下代码片段中,Employee实例是基于age的升序排列。

 CriteriaQuery<Employee> criteriaQuery = criteriaBuilder .createQuery(Employee.class);

 Root<Employee> employee = criteriaQuery.from(Employee.class);

 criteriaQuery.orderBy(criteriaBuilder.asc(employee.get(Employee_.age)));

 em.createQuery(criteriaQuery).getResultList();

对应SQL: SELECT * FROM Employee ORDER BY age ASC

分组

CriteriaQuery 实例的groupBy 方法用于基于Expression的结果分组。查询通过设置额外表达式,以后调用having方法。下面代码片段中,查询按照Employee类的name属性分组,且结果以字母N开头:

    CriteriaQuery<Tuple> cq = criteriaBuilder.createQuery(Tuple.class); 

  Root<Employee> employee = cq.from(Employee.class);

  cq.groupBy(employee.get(Employee_.name));

  cq.having(criteriaBuilder.like(employee.get(Employee_.name), "N%"));

  cq.select(criteriaBuilder.tuple(employee.get(Employee_.name),criteriaBuilder.count(employee)));

  TypedQuery<Tuple> q = em.createQuery(cq);

  List<Tuple> result = q.getResultList();

对应SQL: SELECT name, COUNT(*) FROM employee GROUP BY name HAVING name like ‘N%’

查询投影

Criteria查询的结果与在Critiria查询创建中指定的一样。结果也能通过把查询根传入 CriteriaQuery.select中显式指定。Criteria查询也给开发者投影各种结果的能力。

使用construct()

使用该方法,查询结果能由非实体类型组成。

在下面的代码片段中,为EmployeeDetail类创建了一个Criteria查询对象,而EmployeeDetail类并不是实体类型。

    CriteriaQuery<EmployeeDetails> criteriaQuery = criteriaBuilder.createQuery(EmployeeDetails.class);

  Root<Employee> employee = criteriaQuery.from(Employee.class);

  criteriaQuery.select(criteriaBuilder.construct(EmployeeDetails.class, employee.get(Employee_.name), employee.get(Employee_.age)));

  em.createQuery(criteriaQuery).getResultList();

对应SQL: SELECT name, age FROM employee

返回Object[]的查询

Criteria查询也能通过设置值给CriteriaBuilder.array方法返回Object[]的结果。

下面的代码片段中,数组大小是2(由String和Integer组成)。

    CriteriaQuery<Object[]> criteriaQuery = criteriaBuilder.createQuery(Object[].class);

  Root<Employee> employee = criteriaQuery.from(Employee.class);

  criteriaQuery.select(criteriaBuilder.array(employee.get(Employee_.name), employee.get(Employee_.age)));

  em.createQuery(criteriaQuery).getResultList();

对应SQL: SELECT name, age FROM employee

返回元组(Tuple)的查询

数据库中的一行数据或单个记录通常称为元组。通过调用CriteriaBuilder.createTupleQuery()方法,查询可以用于元组上。CriteriaQuery.multiselect方法传入参数,它必须在查询中返回。

    CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();

   Root<Employee> employee = criteriaQuery.from(Employee.class);

   criteriaQuery.multiselect(employee.get(Employee_.name).alias("name"), employee.get(Employee_.age).alias("age"));

   em.createQuery(criteriaQuery).getResultList();

对应SQL: SELECT name, age FROM employee

结论

Criteria查询是一种以更加面向对象的方式查询数据库的方法、在本文中,我讨论了JPA2中类型安全的Criteria查询,以及对于理解Criteria查询非常重要的元模型的概念。也讨论了Criteria查询中的各种API。

Leave a Reply

Your email address will not be published. Required fields are marked *

lWoHvYe 无悔,专一