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

在上一篇《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实现类,就是这么的简单。

——End——
更多精彩分享,可扫码关注微信公众号哦。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值