美文网首页
MiniMall:如何优雅地实现错综复杂的条件查询

MiniMall:如何优雅地实现错综复杂的条件查询

作者: Anbang713 | 来源:发表于2020-04-25 08:29 被阅读0次

    在上一篇《MiniMall:CRUD的代码是不可能写得?是的,我都帮你写好了》博客中,我们主要分析了MiniMall项目中各个实现层对CRUD的代码封装,个人觉得主要内容都介绍到了,但是有一点还想再具体说说,那就是今天的主题,如何优雅地实现错综复杂的条件查询。这个是什么意思呢?我们看看账务微服务下账单模块搜索界面的搜索条件:

    这么看是不复杂的对不对?所有的查询条件都能在实体对象中找到,只需要在账单主表中进行条件过滤查询即可。没错,确实是这样的。但是这里的复杂是复杂在前端传给控制层,控制层又传给业务层,业务层再调用持久层最终完成数据查询的过程。

    1. 持久层的规范查询语义

    在持久层的接口定义中,账单持久层接口StatementRepository间接地继承了JpaSpecificationExecutor用来支持分页和规范查询。JpaSpecificationExecutor接口中有这样的一个方法,我们最终也是通过该方法实现分页查询和规范查询的。

    Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
    

    方法有两个入参,第一个参数就是规范查询定义,第二个参数是分页参数。

    • Specification

    这是一个接口,接口中有一个方法:

    @Nullable
    Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);
    

    那我们就知道了,最终我们要调用findAll方法就要传入一个Specification实现类,该实现类实现了toPredicate方法,而该方法的返回值Predicate就是一个条件表达式。了解了这一点很重要,其实我们要做的就是封装一个构造条件表达式的东西。

    • Pageable

    这也是一个接口,用来描述分页和排序规则。

    2. QueryDefinition

    QueryDefinition用来描述前端的查询定义,其代码如下:

    @Data
    @NoArgsConstructor
    public class QueryDefinition {
    
        private int page = 1;
        private int pageSize = 10;
        private String keyword;
        private Map<String, Object> filter = new HashMap<>();
        private List<Order> orders = new ArrayList<>();
        private boolean querySummary = false;
        private List<String> fetchParts = new ArrayList<>();
    
        public Map<String, Object> getFilter() {
            return this.filter == null ? new HashMap<>() : this.filter;
        }
    
        public void setSort(String sort) {
            orders.addAll(JSONUtil.toList(JSONUtil.parseArray(sort), Order.class));
        }
    
        public int getCurrentPage() {
            return page <= 1 ? 0 : page - 1;
        }
    }
    
    • page:当前页;
    • pageSize:每页大小;
    • keyword:关键字,每个业务模块通常会有一个关键字查询。比如账单模块的关键字就是单号,项目模块的关键字就是代码 or 名称;
    • filter:查询条件,Map集合中的key可以是任一值,和具体的某一个业务模块实体类属性无关,这就是错综复杂的地方;
    • orders:排序规则;
    • querySummary:是否汇总数据,通常是状态汇总,比如账单搜索界面未生效、已生效状态的数据统计;
    • fetchParts:是否获取关联数据,比如账单搜索界面要展示合同、项目、商户信息,就要去招商微服务获取。

    理解前端的查询定义QueryDefinition和持久层的查询定义Specification,那接下里就是怎么将QueryDefinition转换成Specification了。

    3. 业务层query方法封装

    在业务层的抽象实现类AbstractServiceImpl中对query方法进行了封装,我们先来看实现代码:

    @Override
    public QueryResult<T> query(QueryDefinition definition) {
        PageRequest pageRequest = getPageRequest(definition);
        Page<T> page = getRepository().findAll(getSpecification(definition), pageRequest);
    
        QueryResult<T> result = new QueryResult<>();
        result.setTotal(page.getTotalElements());
        result.getRecords().addAll(page.getContent());
        return result;
    }
    

    方法入参QueryDefinition就是前端传过来的查询定义。然后通过getPageRequest()方法构造分页和排序规则,通过getSpecification()方法构造持久层规范查询定义。

    3.1 getPageRequest

    private PageRequest getPageRequest(QueryDefinition definition) {
        List<Order> orders = definition.getOrders();
        if (orders.isEmpty()) {
            orders.add(new Order("uuid", OrderDirection.asc));
        }
        List<Sort.Order> sortOrders = new ArrayList<>();
        for (Order order : orders) {
            sortOrders.add(getOrderBuilder().build(order.getDirection(), order.getProperty()));
        }
        return PageRequest.of(definition.getCurrentPage(), definition.getPageSize(), Sort.by(sortOrders));
    }
    

    这个没什么好说的,就是将QueryDefinition中的Order转换成Sort.Order,其中getOrderBuilder()方法返回一个OrderBuilder实现类,整个项目中,我们也提供了一个默认的实现类DefaultOrderBuilder,每个业务模块可实现OrderBuilder接口提供个性化的字段排序规则。

    public OrderBuilder getOrderBuilder() {
        return new DefaultOrderBuilder();
    }
    

    3.2 getSpecification

    private Specification<T> getSpecification(QueryDefinition definition) {
        return new Specification<T>() {
    
            @Override
            public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
                                         CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicates = new ArrayList<>();
                Map<String, Object> params = definition.getFilter();
                for (String property : params.keySet()) {
                    Predicate predicate = getSpecificationBuilder().build(root, query, criteriaBuilder,
                                                                          property, params.get(property));
                    if (predicate != null) {
                        predicates.add(predicate);
                    }
                }
                // 关键字查询
                if (StringUtils.isNotBlank(definition.getKeyword())) {
                    Predicate predicate = getSpecificationBuilder().build(root, query, criteriaBuilder,
                                                                          "keyword", definition.getKeyword());
                    if (predicate != null) {
                        predicates.add(predicate);
                    }
                }
                return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
            }
    
        };
    }
    

    在该方法中,循环遍历QueryDefinitionfilter的每一个查询条件,然后由SpecificationBuilder构造一个谓语表达式。

    public abstract SpecificationBuilder getSpecificationBuilder();
    
    • SpecificationBuilder接口
    public interface SpecificationBuilder {
    
        Predicate build(Root root, CriteriaQuery query, CriteriaBuilder cb, String property, Object value);
    }
    

    由于每个业务模块的查询条件是不一样的,项目中并没有提供一个默认的SpecificationBuilder实现,而是由具体的业务模块提供,以账单模块举例:

    @Component
    public class StatementSpecificationBuilder implements SpecificationBuilder {
    
        @Override
        public Predicate build(Root root, CriteriaQuery query, CriteriaBuilder cb, String property, Object value) {
            if (value == null || (value instanceof List && ((List) value).isEmpty()))
                return null;
            if ("keyword".equals(property)) {
                String pattern = "%" + value + "%";
                return cb.like(root.get("billNumber"), pattern);
            } else if ("state".equals(property)) {
                if (value instanceof List) {
                    List<Predicate> predicates = new ArrayList<>();
                    ((List) value).stream().forEach(val -> predicates.add(cb.equal(root.get("state"), BizState.valueOf(val.toString()))));
                    return cb.or(predicates.toArray(new Predicate[]{}));
                } else {
                    return cb.equal(root.get("state"), BizState.valueOf(value.toString()));
                }
            } else if ("payState".equals(property)) {
                return cb.equal(root.get("payState"), PayState.valueOf(value.toString()));
            } else if ("storeUuid".equals(property)) {
                return cb.equal(root.get("storeUuid"), value);
            } else if ("tenantUuid".equals(property)) {
                return cb.equal(root.get("tenantUuid"), value);
            } else if ("contractUuid".equals(property)) {
                return cb.equal(root.get("contractUuid"), value);
            } else if ("dateRange".equals(property)) {
                LinkedHashMap<String, String> valueMap = (LinkedHashMap) value;
                Date beginDate = DateUtil.parse(valueMap.get("beginDate"));
                Date endDate = DateUtil.parse(valueMap.get("endDate"));
                if (beginDate != null && endDate == null) {
                    return cb.greaterThanOrEqualTo(root.get("accountDate"), beginDate);
                } else if (beginDate == null && endDate != null) {
                    return cb.lessThanOrEqualTo(root.get("accountDate"), endDate);
                } else if (beginDate != null && endDate != null) {
                    return cb.and(cb.greaterThanOrEqualTo(root.get("accountDate"), beginDate), cb.lessThanOrEqualTo(root.get("accountDate"), endDate));
                }
            }
            return null;
        }
    }
    

    至此,关于如何优雅地实现错综复杂的条件查询架构已经理清楚,每个业务模块需要做的就是提供一个SpecificationBuilder实现类,就是这么的简单。

    相关文章

      网友评论

          本文标题:MiniMall:如何优雅地实现错综复杂的条件查询

          本文链接:https://www.haomeiwen.com/subject/fhkhwhtx.html