该文翻译自网络,原文地址:
http://www.developer.com/java/ent/article.php/3902911/Querying-in-JPA-2-Typesafe-and-Object-Oriented.htm
JPA流行的主要原因之一是JPQL,它支持面向对象的机制来查询数据库。但是JPQL有一个重大缺陷:作为查询字符串构建的JPQL查询在编译时不会被计算。JPA2.0引入了criteria 查询:一种类型安全和更面向对象的查询。使用criteria 查询,开发人员可以在编译时检查查询的正确与否。该特性以前只在像Hibernate这样的某些专用框架中可用。
在本文中,我将解释criteria 查询,并且浏览对构建criteria 查询非常有必要的元模型(metamodel)的概念。另外也会讨论criteria 查询的各种API。
元模型概念
在JPA2.0中,criteria 查询是以元模型的概念为基础的,元模型是为具体持久化单元的受管实体定义的,这些实体可以是实体类,嵌入类或者映射的父类。简答点说,提供受管实体元信息的类就是元模型类。描述受管类的状态和它们之间的关系的静态元模型类可以:
l 从注解处理器产生
l 从程序产生
l 用EntityManager访问
考虑定义在包com.demo.entities中的实体类Employee
,假设该实体有诸如id,name和age的基本属性,还有与类Address的OneToMany关联。
package com.demo.entities;
Java代码
- @Entity
- @Table
- public class Employee{
- private int id;
- private String name;
- private int age;
- @OneToMany
- private List<Address> addresses;
- // Other code…
- }
Employee类(com.demo.entities
包中定义)的标准元模型类的名字将是使用 javax.persistence.StaticMetamodel
注解的
Employee_。元模型类的属性全部是static和public的。Employee的每一个属性都会使用在JPA2规范中描述的以下规则在相应的元模型类中映射:
l 诸如id,name和age的非集合类型,会定义静态属性SingularAttribute<A, B> b,这里b是定义在类A中的类型为B的一个对象。
l 对于Addess这样的集合类型,会定义静态属性ListAttribute<A, B> b,这里List对象b是定义在类A中类型B的对象。其它集合类型可以是SetAttribute
, MapAttribute
或 CollectionAttribute
类型。
以下是用注解处理器产生的元模型类:
package com.demo.entities;
Java代码
- import javax.annotation.Generated;
- import javax.persistence.metamodel.SingularAttribute;
- import javax.persistence.metamodel.ListAttribute;
- import javax.persistence.metamodel.StaticMetamodel;
- @Generated("EclipseLink-2.1.0.v20100614-r7608 @ Tue Jul 27 10:13:02 IST 2010")
- @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编译器无法验证其正确性。例如:下面的代码会通过编译测试:
Java代码
- 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的元模型类的代码如下:
Java代码
- //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岁的员工:
Java代码
- 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 实例
CriteriaQuery 对象必须在实体类型或嵌入式类型上的Criteria 查询上起作用。它通过调用 CriteriaBuilder, createQuery 或CriteriaBuilder.createTupleQuery 获得。CriteriaBuilder就像CriteriaQuery 的工厂一样。CriteriaBuilder工厂类是调用EntityManager.getCriteriaBuilder 或 EntityManagerFactory.getCriteriaBuilder而得。 Employee实体的 CriteriaQuery 对象以下面的方式创建:
Java代码
- CriteriaBuilder criteriaBuilder = emf.getCriteriaBuilder();
- CriteriaQuery<Employee> criteriaQuery = cb.createQuery(Employee.class);
QueryRoot
AbstractQuery是CriteriaQuery 接口的父类。它提供得到查询根的方法。Criteria查询的查询根定义了实体类型,能为将来导航获得想要的结果,它与SQL查询中的FROM子句类似。
Root实例也是类型化的,且定义了查询的FROM子句中能够出现的类型。查询根实例能通过传入一个实体类型给 AbstractQuery.from方法获得。Criteria查询,可以有多个查询根。Employee实体的查询根对象可以用以下的语法获得:
Java代码
- 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岁的员工实例
Java代码
- 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<Employee>的元模型实例通过调用Metamodel.entity(Employee.class)而获得,其被传入 CriteriaQuery.from 获得查询根。
Java代码
- Metamodel metamodel = em.getMetamodel();EntityType<Employee>
- Employee_ = metamodel.entity(Employee.class);
- Root<Employee> empRoot = criteriaQuery.from(Employee_);
也有可能调用Root.getModel方法获得元模型信息。类型 EntityType<Dept>的实例Dept_和name属性可以调用getSingularAttribute 方法获得,它与String文本进行比较:
Java代码
- 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");
对应的 SQL: SELECT * FROM dept WHERE name = 'Ecomm'
Expression
Expression对象用在查询语句的select,where和having子句中,该接口有 isNull, isNotNull 和 in方法,下面的代码片段展示了Expression.in的用法,employye的年龄检查在20或24的。
Java代码
- 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执行获得结果记录。
Java代码
- criteriaQuery.where(criteriaBuilder.and(criteriaBuilder.like(employee.get(Employee_.name), "M%"), criteriaBuilder.equal(employee.get(Employee_.age), 25)));
- em.createQuery(criteriaQuery).getResultList();
对应 SQL: SELECT * FROM employee WHERE name LIKE 'M%' AND age = 25
连接查询
在SQL中,连接跨多张表以获取查询结果,类似的实体连接通过调用 From.join 执行,连接帮助从一个实体导航到另一个实体以获得查询结果。
Root的join方法返回一个 Join<Dept, Employee>类型(也可以是SetJoin,,ListJoin,MapJoin 或者 CollectionJoin类型)。默认情况下,连接操作使用内连接,而外连接可以通过在join方法中指定JoinType参数为LEFT或RIGHT来实现。
Java代码
- 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);
对应的SQL: SELECT * FROM employee e, dept d WHERE e.deptId = d.id and d.id = 1
抓取连接
当涉及到collection属性时,抓取连接对优化数据访问是非常有帮助的。这是通过预抓取关联对象和减少懒加载开销而达到的。使用 criteria 查询,fetch方法用于指定关联属性
Fetch连接的语义与Join是一样的,因为Fetch操作不返回Path对象,所以它不能将来在查询中引用。在以下例子中,查询Dept对象时employeeCollection对象被加载,这不会有第二次查询数据库,因为有懒加载。
Java代码
- 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
路径表达式
Root实例,Join实例或者从另一个Path对象的get方法获得的对象使用get方法可以得到Path对象,当查询需要导航到实体的属性时,路径表达式是必要的。Get方法接收的参数是在实体元模型类中指定的属性。Path对象一般用于Criteria查询对象的select或where方法。例子如下:
Java代码
- 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:
Java代码
- 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();
- Corresponding SQL: SELECT * FROM Employee WHERE age = 24
排序结果
Criteria查询的结果能调用CriteriaQuery.orderBy方法排序,该方法接收一个Order对象做为参数。通过调用 CriteriaBuilder.asc 或 CriteriaBuilder.Desc,Order对象能被创建。以下代码片段中,Employee实例是基于age的升序排列。
Java代码
- 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);
Java代码
- Root<Employee> employee = cq.from(Employee.class);
- cq.groupBy(employee.get(Employee_.name));
- cq.having(criteriaBuilder.like(employee.get(Employee_.name), "N%"));
- .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 employeeGROUP BY name HAVING name like 'N%'
查询投影
Criteria查询的结果与在Critiria查询创建中指定的一样。结果也能通过把查询根传入 CriteriaQuery.select中显式指定。Criteria查询也给开发者投影各种结果的能力。
使用construct()
使用该方法,查询结果能由非实体类型组成。在下面的代码片段中,为EmployeeDetail类创建了一个Criteria查询对象,而EmployeeDetail类并不是实体类型。
Java代码
- 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();
- Corresponding SQL: SELECT name, age FROM employee<span style="white-space: normal;"> </span>
返回Object[]的查询
Criteria查询也能通过设置值给CriteriaBuilder.array方法返回Object[]的结果。下面的代码片段中,数组大小是2(由String和Integer组成)。
Java代码
- 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方法传入参数,它必须在查询中返回。
Java代码
- 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。