Spring Data JPA 的 Specifications动态查询

0.0 概述

JPA 2 引入了criteria API来构建查询语句中的where条件。这些查询条件用CriteriaBuilder来构建,构建的每个条件都是Predicate对象。

Specification类实现了该API。通过继承JpaSpecificationExecutor接口,就可以使用Specification进行查询。

1.0 适用范围

  • 动态构建查询语句
  • 支持所有的查询条件
  • 支持子查询、连接查询、排序、分页
  • 不支持自定义模型,无法做到将查询的结果封装为自定义的model,或类似List<String>的单列结果。只能返回dao定义的entity。
    即,只支持select * from ...

1.1 JpaSpecificationExecutor接口方法介绍

public interface JpaSpecificationExecutor<T> {
  
    // 查询单个对象
    Optional<T> findOne(@Nullable Specification<T> spec);

    // 查询列表
    List<T> findAll(@Nullable Specification<T> spec);

    // 查询全部,分页;可以使用PageRequest#of(int, int, Sort)方法创建pageable对象实现分页+排序
    Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);

    // 查询全部,排序
    List<T> findAll(@Nullable Specification<T> spec, Sort sort);

    // 统计查询
    long count(@Nullable Specification<T> spec);
  
}

1.2 Specification

Specification类是一个用于构建动态查询条件的抽象类。

public interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder);
}

1.2.1 toPredicate方法介绍

Specification类通过实现抽象方法Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder)来构建动态查询条件,查询条件最终会生成一个查询条件对象(Predicate对象)

  • Root类:用于获取需要查询的列
    • 如:root.get("lastName")
  • CriteriaQuery类:用于自定义查询方式
    • 如:query.distinct(true).select(...).where(...).groupBy(...).having(...).getRestriction()。最后通过getRestriction()方法,可以获得一个Predicate对象
    • 详见
  • CriteriaBuilder类:构造查询条件(即,用于构建Predicate对象),内部封装了很多的查询条件
    • 如:criteriaBuilder.and(...)criteriaBuilder.equal(...)criteriaBuilder.greaterThan)criteriaBuilder.greaterThanOrEqualTo)等,返回的结果是一个Predicate对象
    • 详见

1.2.2 继承关系

Root类、CriteriaQuery类、CriteriaBuilder类都在javax.persistence.criteria包下,其继承关系如下:

1.3 示例

1.3.1 简单示例

(1) 编写查询条件
@Test
public void testSimpleCbSpecification() {
    Specification<RetPrdInfo> specification = (root, query, cb) -> {
        // 等于
        Predicate predicate1 = cb.equal(root.get("crToolRunMode"), "CLN");
        // 不等于
        Predicate predicate2 = cb.notEqual(root.get("prdSeqId"), "L000001");
        // 大于
        Predicate predicate3 = cb.greaterThan(root.get("prdQty"), 1);
        // 大于等于
        Predicate predicate4 = cb.greaterThanOrEqualTo(root.get("prdStdQty"), 1);
        // 小于
        Predicate predicate5 = cb.lessThan(root.get("prdQty"), 2);
        // 小于等于
        Predicate predicate6 = cb.lessThanOrEqualTo(root.get("prdStdQty"), 2);
        // between
        Timestamp startTime = Timestamp.valueOf(LocalDateTime.of(2021, 1, 1, 8, 0, 0));
        Timestamp endTime = Timestamp.valueOf(LocalDateTime.of(2022, 1, 1, 8, 0, 0));
        Predicate predicate7 = cb.between(root.get("evtTimestamp"), startTime, endTime);
        // 非空
        Predicate predicate8 = cb.isNotNull(root.get("prty"));
        // 为空
        Predicate predicate9 = cb.isNull(root.get("costCode"));
        // 模糊查询
        Predicate predicate10 = cb.like(root.get("tempPrdSeqId"), "L%");
        // in
        Predicate predicate11 = root.get("evtUsr").in("AA", "BB");

        Predicate[] predicateArray = {predicate1, predicate2, predicate3, predicate4, predicate5, 
            predicate6, predicate7, predicate8, predicate9, predicate10, predicate11};
        return query.where(predicateArray).getRestriction();
    };
    List<RetPrdInfo> retPrdInfoList = retPrdInfoDao.findAll(specification);
    retPrdInfoList.stream().forEach(System.out::println);
}

生成的sql语句:

select 略
from ret_prd_info
where cr_tool_run_mode = ?
    and prd_seq_id <> ?
    and prd_qty > 1
    and prd_std_qty >= 1
    and prd_qty < 2
    and prd_qty <= 2
    and (evt_timestamp between ? and ?)
    and (prty is not null)
    and (cost_code is null)
    and (temp_prd_seq_id like ?)
  	and (retprdinfo0_.evt_usr in (?, ?));
(2) 查询条件的连接

1️⃣ ​写法一

@Test
public void testConjunctionSpecification1() {
    Specification<RetPrdInfo> specification = (root, query, cb) -> {
        Predicate predicate1 = cb.greaterThan(root.get("prdQty"), 1);
        Predicate predicate2 = cb.lessThan(root.get("prdQty"), 2);
        Predicate orPredicate1 = cb.or(predicate1, predicate2); // 使用or连接两个条件

        Predicate predicate3 = cb.greaterThanOrEqualTo(root.get("prdStdQty"), 1);
        Predicate predicate4 = cb.lessThanOrEqualTo(root.get("prdStdQty"), 2);
        Predicate orPredicate2 = cb.or(predicate3, predicate4); // 使用or连接两个条件

        Predicate andPredicate = cb.and(orPredicate1, orPredicate2);

        return query.where(andPredicate).getRestriction();      // 使用and连接两个条件
    };
    List<RetPrdInfo> retPrdInfoList = retPrdInfoDao.findAll(specification);
    retPrdInfoList.stream().forEach(System.out::println);
}

生成的sql语句:

select 略
from ret_prd_info
where prd_qty > 1 and prd_qty < 2
     or prd_std_qty >= 1 and prd_std_qty <= 2

where 后面如果有and、or的条件,则or自动会把左右的查询条件分开,即先执行and,再执行or。

原因就是:and的执行优先级最高。 关系型运算符优先级高到低为:not and or

2️⃣ 写法二

@Test
public void testConjunctionSpecification2() {
    Specification<RetPrdInfo> specification = (root, query, cb) -> {
        Predicate predicate1 = cb.greaterThan(root.get("prdQty"), 1);
        Predicate predicate2 = cb.lessThan(root.get("prdQty"), 2);
        Predicate[] orArray1 = {predicate1, predicate2};
        Predicate orPredicate1 = cb.and(orArray1);              // 用or连接数组中的每个条件

        Predicate predicate3 = cb.greaterThanOrEqualTo(root.get("prdStdQty"), 1);
        Predicate predicate4 = cb.lessThanOrEqualTo(root.get("prdStdQty"), 2);
        Predicate[] orArray2 = {predicate3, predicate4};
        Predicate orPredicate2 = cb.or(orArray2);               // 用or连接数组中的每个条件

        Predicate[] andArray = {orPredicate1, orPredicate2};
        Predicate andPredicate = cb.and(andArray);              // 用and连接数组中的每个条件

        return query.where(andPredicate).getRestriction();
    };
    List<RetPrdInfo> retPrdInfoList = retPrdInfoDao.findAll(specification);
    retPrdInfoList.stream().forEach(System.out::println);
}

3️⃣ 写法三

@Test{
public void testConjunctionSpecification3() {
    Specification<RetPrdInfo> specification = (root, query, cb) -> {
        Predicate predicate1 = cb.greaterThan(root.get("prdQty"), 1);
        Predicate predicate2 = cb.lessThan(root.get("prdQty"), 2);

        Predicate[] predicateArray = {predicate1, predicate2};

        return query.where(predicateArray).getRestriction(); // where中放入predicate数组,使用and连接数组中的每个条件
    };
    List<RetPrdInfo> retPrdInfoList = retPrdInfoDao.findAll(specification);
    retPrdInfoList.stream().forEach(System.out::println);
}}

生成的sql:

select 略
from ret_prd_info
where prd_qty > 1
    and prd_qty < 2
(3) 对查询结果排序
@Test
public void testOrder() {
    Specification<Customer> spec = (root, query, cb) -> cb.equal(root.get("crToolRunMode"), "CLN");
    Sort sort = Sort.by(Sort.Direction.DESC, "id");
    List<Customer> list = customerDao.findAll(spec, sort);
}
(4) 分页查询
@Test
public void testPagging() {
	  Specification<Customer> spec = (root, query, cb) -> cb.equal(root.get("crToolRunMode"), "CLN");
    Pageable pageable = PageRequest.of(0, 20);
    // 可以使用`Pageable pageable = PageRequest.of(0, 20, Sort.by("evtTimestamp"));`实现分页+排序
    Page<Customer> page = customerDao.findAll(spec, pageable);
  	List<Customer> list = page.getContent();      // 该页的内容
    long totalElements = page.getTotalElements(); // 全表的记录数目
}

1.3.2 多条件查询示例(and、equal、like、sort、page)

每一个Predicate就是一个条件

public void queryByCondition(FbpbisdatI inTrx, FbpbisdatO outTrx) {
    FbpbisdatIA iary = inTrx.getIary().get(0);

    Specification<BisData> specification = (root, query, cb) -> {
        List<Predicate> predicateList = new ArrayList<>();

        if (StringUtils.isNotBlank(iary.getDataCate())) {
          predicateList.add(cb.equal(root.get("dataCate"), iary.getDataCate()));
        }
        if (StringUtils.isNotBlank(iary.getDataSeqId())) {
          predicateList.add(cb.like(root.get("dataSeqId").as(String.class), "%" + ia.getMdlId() + "%"));
          // `"%" + ia.getMdlId() + "%"` 可以改写为 `Util.liktStr(ia.getmdlId())`
        }
        // 略

        Predicate[] p = new Predicate[predicateList.size()];
        query.where(predicateList.toArray(p));
        return query.getRestriction();
    };
    Sort sort = Sort.by("dataCate", "dataId", "dataExt");

    List<BisData> bisDataList;
    // 判断是否需要分页
    if (Util.shouldPage(inTrx)) {
        Page<BisData> bisDataPage = bisDataDao.findAll(specification, PageRequest.of(inTrx.getPageNum() - 1, inTrx.getPageSize(), sort));
        // `PageRequest.of(inTrx.getPageNum() - 1, inTrx.getPageSize(), sort)`
        // 可以改写为 `Util.getPageable(inTrx, sort)`

        bisDataList = bisDataPage.getContent();
        long tblCnt = bisDataPage.getTotalElements();

        outTrx.setTblCnt(tblCnt);
    } else {
        bisDataList = bisDataDao.findAll(specification, sort);
    }
    List<FbpbisdatOA> oary = CloneUtil.cloneList(bisDataList, FbpbisdatOA::new);
    outTrx.setOary(oary);
}

1.3.3 子查询示例

public void testSubSpecification() {
    Specification<RetPrdInfo> specification = (root, query, cb) -> {
        // 1).父查询条件
        Predicate parentPredicate = cb.equal(root.get("crToolRunMode"), "CLN");

        // 2).子查询
        Subquery<RetPrdAbn> subQuery = query.subquery(RetPrdAbn.class);
        Root<RetPrdAbn> subRoot = subQuery.from(RetPrdAbn.class);

        subQuery = subQuery.select(subRoot.get("prdSeqIdFk"));

        // 子查询的cb和父查询的cb相同
        Predicate subPredicate1 = cb.equal(subRoot.get("lotIdFk"), "LW0001");
        Predicate subPredicate2 = cb.equal(subRoot.get("lotSpltIdFk"), "00");
        subQuery.where(cb.and(subPredicate1, subPredicate2));

        // 3).将父查询条件和子查询合并
        Predicate parentAndSubConjunctPredicate = root.get("prdSeqId").in(subQuery);

        return query.where(parentPredicate, parentAndSubConjunctPredicate).getRestriction();
    };

    List<RetPrdInfo> retPrdInfoList = retPrdInfoDao.findAll(specification);
    retPrdInfoList.stream().forEach(System.out::println);
}

生成的sql语句:

select
    略
from 
    ret_prd_info
where 
    cr_tool_run_mode = ?
    and (prd_seq_id in 
             (
                select prd_seq_id_fk
                from ret_prd_abn
                where lot_id_fk = ? and retprdabn1_.lot_splt_id_fk = ?
             )
        )

posted @ 2021-09-25 10:20  daheww  阅读(1659)  评论(0编辑  收藏  举报