【一目了然】Spring Data JPA使用Specification动态构建多表查询、复杂查询及排序示例
JPA 使用 Specification 复杂查询和 Criteria 查询
Spring data jpa 的使用与详解(二):复杂动态查询及分页,排序
业务需求:JPA 确实挺好用,给我们提供了 CRUD 的功能,并且用起来也是特别的方便,基本都是一行代码就能完成各种数据库操作,跟 mybatisplus 很像。。。但是在遇到复杂的多条件以及多表查询的时候,总是会首先考虑到手写原生的 SQL。
Specification 算是 JPA 中比较灵活的查询方式,也少不了 Criteria 类型安全和面向对象的优点
环境配置
<!-- jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
代码实现
public class xxxSpecification {
private xxxSpecification() {
}
public static Specification<xxxEntity> buildFromParam(Set<String> xxxList){
return (root,query,builder) -> {
//Predicate: 过滤条件,相当于构造 where 中的条件
ArrayList<Predicate> list = new ArrayList<>();
if(!CollectionUtils.isEmpty(xxxList)){
//root:从元模型中获取相应的字段
CriteriaBuilder.In<String> in = builder.in(root.get("id"));
for(String itemId:xxxList){
in.value(itemId);
}
list.add(in);
}
list.add(builder.equal(root.get("status"), TableStatusEnum.NORMAL_STATUS.getCode()));
Predicate[] predicates = new Predicate[list.size()];
predicates = list.toArray(predicates);
return builder.and(predicates);
};
}
}
以上的代码对应于
原理
JPA 提供动态接口(JpaSpecificationExecutor),利用类型检查方式,进行复杂的条件查询,这个比自己写 SQL 更加安全
package org.springframework.data.jpa.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.lang.Nullable;
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> var1);
List<T> findAll(@Nullable Specification<T> var1);
Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
List<T> findAll(@Nullable Specification<T> var1, Sort var2);
long count(@Nullable Specification<T> var1);
}
Specification
是我们传入进去的查询参数,实际上它是一个接口,并且只有一个方法:
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
元模型
在JPA中,标准查询是以元模型的概念为基础的,元模型是为具体持久化单元的受管实体定义的.这些实体可以是实体类,嵌入类或者映射的父类.提供受管实体元信息的类就是元模型类.
简单的说就是元模型是实体类对应的一个“受管实体
例子:
实体类 Employee(com.demo.entities包中定义)
@Entity
@Table
public class Employee{
private int id;
private String name;
private int age;
@OneToMany
private List<Address> addresses;
// Other code…
}
Employee类的标准元模型类的名字是 Employee_
import javax.annotation.Generated;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.ListAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@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;
}
Employee的每一个属性都会使用在JPA2规范中描述的以下规则在相应的元模型类中映射:
- 元模型类的属性全部是static和public的。
- 元模型类的属性全部是static和public的。Employee的每一个属性都会使用在JPA2规范中描述的以下规则在相应的元模型类中映射:
- 对于Addess这样的集合类型,会定义静态属性ListAttribute< A, B> b,这里List对象b是定义在类A中类型B的对象。其它集合类型可以是SetAttribute, MapAttribute 或 CollectionAttribute 类型。
为什么要使用元模型,答:查询类型更加安全
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
CriteriaBuilder 安全查询创建工厂
CriteriaBuilder 安全查询创建工厂,创建 CriteriaQuery,创建查询具体条件 Predicate 等。
CriteriaBuilder是一个工厂对象,安全查询的开始.用于构建JPA安全查询.可以从EntityManager 或 EntityManagerFactory类中获得CriteriaBuilder。
比如:
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery 安全查询主语句
- 它通过调用 CriteriaBuilder, createQuery 或CriteriaBuilder.createTupleQuery 获得。
- CriteriaBuilder就像CriteriaQuery 的工厂一样。
- CriteriaQuery对象必须在实体类型或嵌入式类型上的Criteria 查询上起作用。
- Employee实体的 CriteriaQuery 对象以下面的方式创建:
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);
Root 定义查询的 From 子句中能出现的类型
查询表达式被赋予泛型。一些典型的表达式是:
-
Root<T>, 相当于一个 From 子句,定义查询的 From 子句中能出现的类型
-
Criteria查询的查询根定义了实体类型,能为将来导航获得想要的结果,它与SQL查询中的FROM子句类似。
-
Root实例也是类型化的,且定义了查询的FROM子句中能够出现的类型。
-
查询根实例能通过传入一个实体类型给 AbstractQuery.from方法获得
-
Criteria查询,可以有多个查询根。
-
Employee 实体的查询根对象可以用以下语法获得:
Root<Employee> employee = criteriaQuery.from(Employee.class);
源码
package javax.persistence.criteria;
import javax.persistence.metamodel.EntityType;
public interface Root<X> extends From<X, X> {
EntityType<X> getModel();
}
可以看出 Root 继承自 From
public interface From<Z, X> extends Path<X>, FetchParent<Z, X>
而 From 又继承自 Path
package javax.persistence.criteria;
import java.util.Collection;
import java.util.Map;
import javax.persistence.metamodel.Bindable;
import javax.persistence.metamodel.MapAttribute;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
public interface Path<X> extends Expression<X> {
Bindable<X> getModel();
Path<?> getParentPath();
<Y> Path<Y> get(SingularAttribute<? super X, Y> var1);
<E, C extends Collection<E>> Expression<C> get(PluralAttribute<X, C, E> var1);
<K, V, M extends Map<K, V>> Expression<M> get(MapAttribute<X, K, V> var1);
Expression<Class<? extends X>> type();
<Y> Path<Y> get(String var1);
}
Predicate 过滤条件
- 过滤条件应用到SQL语句的FROM子句中。
- 在criteria 查询中,查询条件通过Predicate 或Expression 实例应用到CriteriaQuery 对象上。
- 这些条件使用 CriteriaQuery .where 方法应用到CriteriaQuery 对象上。
- Predicate 实例也可以用Expression 实例的 isNull, isNotNull 和 in方法获得,复合的Predicate 语句可以使用CriteriaBuilder的and, or andnot 方法构建。
- CriteriaBuilder 也是作为Predicate 实例的工厂,Predicate 对象通过调用CriteriaBuilder 的条件方法( equal,notEqual, gt, ge,lt, le,between,like等)创建。
- 这些条件使用 CriteriaQuery .where 方法应用到CriteriaQuery 对象上。
下面的代码片段展示了Predicate 实例检查年龄大于24岁的员工实例:
Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24);
criteriaQuery.where(condition);
初步结论
CriteriaBuilder: 构造 sql 语句
predicate:构造 where 中的条件语句,过滤条件
root:获取对应元模型的字段属性