在上一篇《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();
}
};
}
在该方法中,循环遍历QueryDefinition
中filter
的每一个查询条件,然后由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
实现类,就是这么的简单。
网友评论