前言
JPA
给我们提供了基础的 CURD
的功能,并且用起来也是特别的方便,基本都是一行代码完成各种数据库操作,但是在复杂的多表查询的时候,我总是遇到各种问题,以前一般都是用原生 SQL 就行查询,原来它自带了复杂查询的 JpaSpecificationExecutor
接口,可以完成各种复杂查询,而且配合 JAVA 8的新特性,使用起来也是特别的方便。
环境:
- Spring Boot - 2.0.0.RELEASE
- Jpa
- jdk - 1.8
- mysql
- lombok
本文学习代码的地址
使用 JpaSpecificationExecutor 复杂查询
了解 JpaSpecificationExecutor
JPA 提供动态接口,利用类型检查的方式,进行复杂的条件查询,这个比自己写 SQL 更加安全。
1 | public interface JpaSpecificationExecutor<T> { |
Specification
是我们传入进去的查询参数,实际上它是一个接口,并且只有一个方法:
1 | public interface Specification<T> { |
使用
创建实体类
- 我现在有三个实体类是关联关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Employee implements Serializable {
(strategy = GenerationType.IDENTITY)
private Long id;
// 拥有级联维护的一方,参考http://westerly-lzh.github.io/cn/2014/12/JPA-CascadeType-Explaining/ (cascade = CascadeType.ALL)
"none", value = ConstraintMode.NO_CONSTRAINT)) (foreignKey = (name =
private EmployeeDetail detail;
// 默认 lazy ,通过懒加载,知道需要使用级联的数据,才去数据库查询这个数据,提高查询效率。 (fetch = FetchType.LAZY)
// 设置外键的问题,参考http://mario1412.github.io/2016/06/27/JPA%E4%B8%AD%E5%B1%8F%E8%94%BD%E5%AE%9E%E4%BD%93%E9%97%B4%E5%A4%96%E9%94%AE/
"jobId", foreignKey = (name = "none", value = ConstraintMode.NO_CONSTRAINT)) (name =
private Job job;
}
public class EmployeeDetail implements Serializable {
(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String phone;
private Integer age;
}
public class Job implements Serializable {
(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
"job") // mappedBy 只有在双向关联的时候设置,表示关系维护的一端,否则会生成中间表A_B (targetEntity = Employee.class, mappedBy =
"none") // 注意这里不能使用 @JoinColumn 中的 @ForeignKey 不然会生成外键 .hibernate.annotations.ForeignKey(name =
private Set<Employee> employees;
}
创建持久化元模型
这个可以不实现,但是在后面实现复杂查询的时候,只能手动输入相关的实体类的属性字段的字符串,然后进行强制转换类型,我认为这样相对来说好维护一些,关于持久化元模型,将在这篇文章最后部分再详细介绍下。
1 | /** |
创建 dao 接口,继承 JpaSpecificationExecutor<T>
:
1 |
|
实现复杂动态查询:
1 | /** |
CriteriaBuilder
有各种操作方法完成查询操作。
进行测试
1 |
|
其实实现起来不是很复杂,但是用起来很舒坦,再完全不用手动拼接字符串,使用面向对象的类型检测,写起来 BUG 也少些。
使用 CriteriaQuery 查询和类型安全检测
使用criteria 查询
例如我现在要实现一个接口,查询在大于或等于某个年纪的员工:
1 |
|
构建CriteriaQuery 实例API说明
CriteriaBuilder 安全查询创建工厂
CriteriaBuilder
安全查询创建工厂,创建CriteriaQuery
,创建查询具体具体条件Predicate
等CriteriaBuilder
是一个工厂对象,安全查询的开始.用于构建JPA安全查询.可以从EntityManager 或EntityManagerFactory
类中获得CriteriaBuilder
.CriteriaBuilder
工厂类是调用EntityManager.getCriteriaBuilder
或 EntityManagerFactory.getCriteriaBuilder
而得。
CriteriaQuery 安全查询主语句
CriteriaQuery对象必须在实体类型或嵌入式类型上的Criteria 查询上起作用。
它通过调用 CriteriaBuilder, createQuery
或CriteriaBuilder.createTupleQuery
获得。
Root 定义查询的From子句中能出现的类型
AbstractQuery是CriteriaQuery 接口的父类。它提供得到查询根的方法。
Criteria查询的查询根定义了实体类型,能为将来导航获得想要的结果,它与SQL查询中的FROM子句类似。
Root实例也是类型化的,且定义了查询的FROM子句中能够出现的类型。
查询根实例能通过传入一个实体类型给 AbstractQuery.from方法获得。
Criteria查询,可以有多个查询根。
1 | Root<Employee> employee = criteriaQuery.from(Employee.class); |
Predicate 过滤条件
过滤条件应用到SQL语句的FROM子句中。因此它是 root
创建的。
在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岁的员工实例:
1 | Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24); |
Employee_
元模型类age
属性,称之为路径表达式。若age
属性与String
文本比较,编译器会抛出错误,这在JPQL中是不可能的。
Predicate[] 多个过滤条件
支持复杂的 条件拼接, or 语句
1 | predicatesList.add(criteriaBuilder.or(criteriaBuilder.equal(root.get(RepairOrder_.localRepairStatus), LocalRepairStatus.repairing),criteriaBuilder.equal(root.get(RepairOrder_.localRepairStatus), LocalRepairStatus.diagnos))); |
最后查询的时候
1 | query.where(cb.or(predicates.toArray(new Predicate[predicates.size()]))); |
TypedQuery执行查询与获取元模型实例
注意,你使用EntityManager创建查询时,可以在输入中指定一个CriteriaQuery对象,它返回一个TypedQuery,它是JPA 2.0引入javax.persistence.Query接口的一个扩展,TypedQuery接口知道它返回的类型。
所以使用中,先创建查询得到TypedQuery,然后通过typeQuery得到结果.
当EntityManager.createQuery(CriteriaQuery)方法调用时,一个可执行的查询实例会创建,该方法返回指定从 criteria 查询返回的实际类型的TypedQuery 对象。
TypedQuery 接口是javax.persistence.Queryinterface.的子类型。在该片段中, TypedQuery 中指定的类型信息是Employee,调用getResultList时,查询就会得到执行 :
1 | TypedQuery<Employee> typedQuery = em.createQuery(criteriaQuery); |
元模型实例通过调用 EntityManager.getMetamodel
方法获得,EntityType<Employee>
的元模型实例通过调用Metamodel.entity(Employee.class)
而获得,其被传入 CriteriaQuery.from
获得查询根。
1 | Metamodel metamodel = em.getMetamodel(); |
Expression 用在查询语句的select,where和having子句中,该接口有 isNull, isNotNull 和 in方法
Expression对象用在查询语句的select,where和having子句中,该接口有 isNull, isNotNull 和 in方法,下面的代码片段展示了Expression.in的用法,employye的年龄检查在20或24的。
1 | CriteriaQuery<Employee> criteriaQuery = criteriaBuilder .createQuery(Employee.class); |
下面也是一个更贴切的例子:
1 | //定义一个Expression |
复合谓词
Criteria Query也允许开发者编写复合谓词,通过该查询可以为多条件测试下面的查询检查两个条件。首先,name属性是否以M开头,其次,employee的age属性是否是25。逻辑操作符and执行获得结果记录。
1 | criteriaQuery.where( |
路径表达式
Root实例,Join实例或者从另一个Path对象的get方法获得的对象使用get方法可以得到Path对象,当查询需要导航到实体的属性时,路径表达式是必要的。
Get方法接收的参数是在实体元模型类中指定的属性。
参数化表达式
在JPQL中,查询参数是在运行时通过使用命名参数语法(冒号加变量,如 :age
传入的。在Criteria查询中,查询参数是在运行时创建ParameterExpression
对象并为在查询前调用TypeQuery,setParameter
方法设置而传入的。
1 | ParameterExpression<Integer> ageParameter = cb.parameter(Integer.class); |
排序结果
1 | // 设置排序规则 |
可以设置多个 order。
分组
1 | /** |
返回元组(Tuple)的查询
查询的时候需要查询 单列
的记录可以使用元组,
1 | CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery(); |
使用 construct()
使用一个不是实体类来装载 查询出来的数据,但是必须要的是实体类必须有相应的构造函数才行,还需要注意的是,该装载类必须继承实体类
首先实现一个装载数据的类,我只用来装载 name, age 两个属性即可,当然需要更多也可以设计的,毕竟我们继承了实体类,如果实体类有的字段我们可以不需要再制造了。
1
2
3
4
5
6
public class EmployeeResult extends Employee {
private String name;
private Integer age;
}编写查询的业务代码,很方便:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* 使用 构造函数 装载查询出来的数据
*
* @return
*/
public List<EmployeeResult> findEmployee() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Employee> query = cb.createQuery(Employee.class);
Root<Employee> root = query.from(Employee.class); // 设置查询根,可以根据查询的类型设置不同的
Join<Employee, EmployeeDetail> join = root.join(Employee_.detail, JoinType.LEFT);
// 使用构造函数 CriteriaBuilder.construct 来完成装载数据
query.select(cb.construct(EmployeeResult.class, join.get(EmployeeDetail_.name), join.get(EmployeeDetail_.age)));
// 设置排序规则
Order order = cb.asc(root.get(Employee_.id));
query.orderBy(order);
TypedQuery typedQuery = em.createQuery(query); // TypedQuery执行查询与获取元模型实例
return typedQuery.getResultList();
}
返回 Object[]
Criteria查询也能通过设置值给CriteriaBuilder.array方法返回 Object[]
的结果。
1 | criteriaQuery.select(criteriaBuilder.array(root.get(xxx),join.get(xxx))); |
关于持久化元模型
在JPA中,标准查询是以元模型的概念为基础的.元模型是为具体持久化单元的受管实体定义的.这些实体可以是实体类,嵌入类或者映射的父类.提供受管实体元信息的类就是元模型类.
描述受管类的状态和他们之间的关系的静态元模型类可以
- 从注解处理器产生
- 从程序产生
- 用EntityManager访问.
元模型类描述持久化类的元数据。如果一个类安装 JPA 2.0 规范精确地描述持久化实体的元数据,那么该元模型类就是 规范的。规范的元模型类是 静态的,因此它的所有成员变量都被声明为 静态的(也是 public的)。
Employee类的标准元模型类的名字将是使用 javax.persistence.StaticMetamodel注解的Employee_。元模型类的属性全部是static和public的。Employee的每一个属性都会使用在JPA2规范中描述的以下规则在相应的元模型类中映射:
- 诸如id,name和age的非集合类型,会定义静态属性SingularAttribute<A, B> b,这里b是定义在类A中的类型为B的一个对象。
- 对于Addess这样的集合类型,会定义静态属性
ListAttribute<A, B> b
,这里List对象b是定义在类A中类型B的对象。其它集合类型可以是SetAttribute
,MapAttribute
或CollectionAttribute
类型。
简单总结下
- 这次搭建的 jpa 框架是自动生成的数据库表,中间出了很多叉子,例如,自动生成了中间表,不想要外键,给你自己生成外键,而且还不好解决,最终通过谷歌终于找到了解决办法,相应的网页,也在代码上标注出来了,还是基础不行。
- 主要理解怎么从
CriteriaBuilder
一步步的在下面创建查询的条件,例如 查询类型root
,查询语句CriteriaQuery
,查询条件Predicate
,这样就很容易构建一个 criteria 查询。 - 了解了 JPA 复杂查询中 Specification 接口,给人第一体验就是完全面向对象,包括类型检查,基本上代码写了一遍就能编译运行它通过。再多条件动态条件查询的时候,利用 java 8 的 Optional 的空指针检查,比较方便,不用再被长长的 SQL 拼接发麻了。