美文网首页
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:如何优雅地实现错综复杂的条件查询

    在上一篇《MiniMall:CRUD的代码是不可能写得?是的,我都帮你写好了》博客中,我们主要分析了MiniMal...

  • 如何优雅地实现分页查询

    分页功能是很常见的功能,特别是当数据量越来越大的时候,分页查询是必不可少的。实现分页功能有很多种方式,如果使用的O...

  • 简短优雅地利用js实现 sleep 函数

    简短优雅地实现 sleep 函数 很多语言都有 sleep 函数,显然 js 没有,那么如何能简短优雅地实现这个方...

  • 如何优雅

    如何优雅?这是一个很流行,也很值得追问的问题。 如何优雅地读书?如何优雅地喝茶?如何优雅地跑步?如何优雅地追求女神...

  • Elasticsearch Search API之(Reques

    本节将详细介绍es Search API的查询主体,定制化查询条件的实现主体。 query 搜索请求体中查询条件使...

  • es

    初始化数据 查询(文档) 基于url实现查询 获取索引类型下的所有数据 根据条件进行查询 基于QueryDSL实现...

  • Yii框架下的where条件查询

    条件查询cond)->all();$cond就是我们所谓的条件,条件的写法也根据查询数据的不同存在差异,那么如何用...

  • 如何优雅地使用 KVO

    如何优雅地使用 KVO 如何优雅地使用 KVO

  • 如何优雅地实现策略模式

    在 XMind iOS 版开发过程中, 需要对不同的布局方式选择不同的类来进行布局,所以需要用到策略模式来处理。在...

  • SpringMVC入门知识6

    特殊类型的参数绑定:包装类型的pojo参数绑定:需求:实现商品查询条件,在商品查询的controller中实现商品...

网友评论

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

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