JpaSpecificationExecutor 介绍 [自用]
JpaSpecificationExecutor 是 JPA 2.0 提供的 Criteria API 的使用封装,可以用于动态生成 Query 来满足我们业务中的各种复杂场景。Spring Data JPA 为我们提供了 JpaSpecificationExecutor 接口,只要简单实现 toPredicate 方法就可以实现复杂的查询。
JpaSpecificationExecutor接口
public interface JpaSpecificationExecutor<T> {
//根据 Specification 条件查询单个对象
T findOne(Specification<T> var1);
//根据 Specification 条件查询一个list集合
List<T> findAll(Specification<T> var1);
//根据 Specification 条件,分页查询
Page<T> findAll(Specification<T> var1, Pageable var2);
//根据 Specification 条件,带排序的查询结果
List<T> findAll(Specification<T> var1, Sort var2);
//根据 Specification 条件,查询条数
long count(Specification<T> var1);
}
查看 Specifications 源码
public interface Specification<T> {
Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);
}
从这里可以看出,每个调用的地方都需要,创建 Specification 的实现类,而 JpaSpecificationExecutor 是针对 Criteria API 进行了 predicate 标准封装,帮我们封装了通过 EntityManager 的查询和使用细节,使操作 Criteria 更加便利了一些。所以我们要掌握一下 Predicate、Root、CriteriaQuery、CriteriaBuilder 是什么?
Criteria概念的简单介绍
1.Root
代表了可以查询和操作的实体对象的根,如果将实体对象比喻成表名,那 root 里面就是这张表里面的字段,这不过是 JPQL 的实体字段而已。通过里面的 Path get(String attributeName),来获得我们想操作的字段。
2.CriteriaQuery<?>query
代表一个specific的顶层查询对象,它包含着查询的各个部分,比如:select、from、where、group by、order by等。CriteriaQuery对象只对实体类型或嵌入式类型的Criteria查询起作用,简单理解,它提供了查询ROOT的方法。常用的方法有:
CriteriaQuery<T> where(Predicate... restrictions);
CriteriaQuery<T> select(Selection<? extends T> selection);
CriteriaQuery<T> having(Predicate... restrictions);
3.CriteriaBuilder cb
用来构建 CritiaQuery 的构建器对象,其实就相当于条件或者是条件组合,并以 Predicate 的形式返回。下面是构建简单的 Predicate 示例:
简单来说
Root:查询哪个表
CriteriaQuery:查询哪些字段,排序是什么
CriteriaBuilder:字段之间是什么关系,如何生成一个查询条件,每一个查询条件都是什么方式
Predicate(Expression):单独每一条查询条件的详细描述
JpaSpecificationExecutor 使用案例
新建两个实体
@Entity(name = "UserInfoEntity")
@Table(name = "user_info", schema = "test")
public class UserInfoEntity implements Serializable {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "first_name", nullable = true, length = 100)
private String firstName;
@Column(name = "last_name", nullable = true, length = 100)
private String lastName;
@Column(name = "telephone", nullable = true, length = 100)
private String telephone;
@Column(name = "create_time", nullable = true)
private Date createTime;
@Column(name = "version", nullable = true)
private String version;
@OneToOne(optional = false,fetch = FetchType.EAGER)
@JoinColumn(referencedColumnName = "id",name = "address_id",nullable = false)
@Fetch(FetchMode.JOIN)
private UserReceivingAddressEntity addressEntity;
......
}
@Entity
@Table(name = "user_receiving_address", schema = "test")
public class UserReceivingAddressEntity implements Serializable {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "user_id", nullable = false)
private Integer userId;
@Column(name = "address_city", nullable = true, length = 500)
private String addressCity;
......
}
UserRepository 需要继承 JpaSpecificationExecutor
public interface UserRepository extends JpaSpecificationExecutor<UserInfoEntity> {
}
调用 UserInfoManager 的写法
我们演示一下直接用 lambda 使用 Root 和 CriteriaBuilder 做一个简单的不同条件的查询和链表查询。
@Component
public class UserInfoManager {
@Autowired
private UserRepository userRepository;
public Page<UserInfoEntity> findByCondition(UserInfoRequest userParam,Pageable pageable){
return userRepository.findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<Predicate>();
if (StringUtils.isNoneBlank(userParam.getFirstName())){
//liked的查询条件
predicates.add(cb.like(root.get("firstName"),"%"+userParam.getFirstName()+"%"));
}
if (StringUtils.isNoneBlank(userParam.getTelephone())){
//equal查询条件
predicates.add(cb.equal(root.get("telephone"),userParam.getTelephone()));
}
if (StringUtils.isNoneBlank(userParam.getVersion())){
//greaterThan大于等于查询条件
predicates.add(cb.greaterThan(root.get("version"),userParam.getVersion()));
}
if (userParam.getBeginCreateTime()!=null&&userParam.getEndCreateTime()!=null){
//根据时间区间去查询 predicates.add(cb.between(root.get("createTime"),userParam.getBeginCreateTime(),userParam.getEndCreateTime()));
}
if (StringUtils.isNotBlank(userParam.getAddressCity())) {
//联表查询,利用root的join方法,根据关联关系表里面的字段进行查询。
predicates.add(cb.equal(root.join("addressEntityList").get("addressCity"), userParam.getAddressCity()));
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}, pageable);
}
}
我们再来看一个不常见的复杂查询的写法,来展示一下 CriteriaQuery 的用法(强烈不推荐哦,和上面比起来太不优雅了
public List<MessageRequest> findByConditions(String name, Integer price, Integer stock) {
messageRequestRepository.findAll((Specification<MessageRequest>) (Root, query, criteriaBuilder) -> {
//这里用 List 存放多种查询条件,实现动态查询
List<Predicate> predicatesList = new ArrayList<>();
//name 模糊查询,like 语句
if (name != null) {predicatesList.add(criteriaBuilder.and(criteriaBuilder.like(Root.get("name"),"%" + name + "%")));}
// itemPrice 小于等于 <= 语句
if (price != null) {predicatesList.add(criteriaBuilder.and(criteriaBuilder.le(Root.get("price"), price)));}
//itemStock 大于等于 >= 语句
if (stock != null) {predicatesList.add(criteriaBuilder.and(criteriaBuilder.ge(Root.get("stock"), stock)));}
//where() 拼接查询条件
query.where(predicatesList.toArray(new Predicate[predicatesList.size()]));
//返回通过 CriteriaQuery 拼装的 Predicate
return query.getRestriction();
});
Specification 工作中的一些扩展
我们在实际工作中会发现,如果上面的逻辑,简单重复写总感觉是不是可以抽出一些公用方法呢,此时引入一种工厂模式,帮我们做一些事情,可以让代码更加优雅。基于 JpaSpecificationExecutor 的思路,我们创建一个 SpecificationFactory.Java 内容如下:
public final class SpecificationFactory {
/**
* 模糊查询,匹配对应字段
*/
public static Specification containsLike(String attribute, String value) {
return (root, query, cb)-> cb.like(root.get(attribute), "%" + value + "%");
}
/**
* 某字段的值等于 value 的查询条件
*/
public static Specification equal(String attribute, Object value) {
return (root, query, cb) -> cb.equal(root.get(attribute),value);
}
/**
* 获取对应属性的值所在区间
*/
public static Specification isBetween(String attribute, int min, int max) {
return (root, query, cb) -> cb.between(root.get(attribute), min, max);
}
public static Specification isBetween(String attribute, double min, double max) {
return (root, query, cb) -> cb.between(root.get(attribute), min, max);
}
public static Specification isBetween(String attribute, Date min, Date max) {
return (root, query, cb) -> cb.between(root.get(attribute), min, max);
}
/**
* 通过属性名和集合实现 in 查询
*/
public static Specification in(String attribute, Collection c) {
return (root, query, cb) ->root.get(attribute).in(c);
}
/**
* 通过属性名构建大于等于 Value 的查询条件
*/
public static Specification greaterThan(String attribute, BigDecimal value) {
return (root, query, cb) ->cb.greaterThan(root.get(attribute),value);
}
public static Specification greaterThan(String attribute, Long value) {
return (root, query, cb) ->cb.greaterThan(root.get(attribute),value);
}
......
}
PS:可以根据实际工作需要和场景进行不断扩充
调用实例1:
userRepository.findAll(
SpecificationFactory.containsLike("firstName", userParam.getLastName()),
pageable);
配合 Specifications 使用,调用实例2:
userRepository.findAll(Specifications.where(
SpecificationFactory.containsLike("firstName", userParam.getLastName()))
.and(SpecificationFactory.greaterThan("version",userParam.getVersion())),
pageable);