需求场景:
不管业务多么复杂,系统架构多么复杂,最终都是对各类数据库的增删改查,现阶段研发的协同研发平台采用微服务架构,ORM
层采用spring-boot-starter-data-jpa
,比如,系统管理服务
中的用户管理
需要支持对用户的按照多个不同字段组合查询,前端的页面往往是提供一个查询表单,如username
,gender
,birthdate
,age
等,遵循restful
约定,后端提供的接口通常是Get
请求类型的,为了适配前端的多参数组合查询请求,最先想到的是这样的:
@RestController("/user")
public class UserController{
@ApiOperation(value = "查询用户列表")
@GetMapping
public PageQueryResponseData list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "25") Integer limit,
@RequestParam(defaultValue = "")
String username,
@RequestParam(defaultValue = "")
String gender,
@RequestParam(defaultValue = "")
String birthdate,
@RequestParam(defaultValue = "")
String age, ) {
PageQueryResponseData result = userService.listToVO(page, limit, params);
return result;
}
}
这样带来的局限性:
- 没有扩展性;
service
层充斥着大量的if/else
代码,目的仅仅是为了聚合查询条件- 各个参数的查询运算符没有描述,比如
username
对应的是数据库中的like
还是equal
查询
一种解决方案:
- 定义一个
bean
组合所有的可能的查询条件,接口中直接通过bean
接收前端的参数,这样接口简化成这样:
@ApiOperation(value = "查询用户列表")
@ApiImplicitParams({
@ApiImplicitParam(name = "page", value = "第几页", required = true, dataType = "Integer"),
@ApiImplicitParam(name = "limit", value = "每一页显示条数", required = true, dataType = "Integer")
})
@LogAnnotation(isSaveRequestData = true, isSaveResponseData = true)
@GetMapping
public PageQueryResponseData list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "25") Integer limit,
@ModelAttribute DefaultUserQryWrapper params) {
PageQueryResponseData result = userService.listToVO(page, limit, params);
return result;
}
- 封装的查询
bean
:DefaultUserQryWrapper.java
@Data
@ApiModel(value="用户查询封装,默认分页,page,limit")
@Builder
public final class DefaultUserQryWrapper implements BaseQryWrapperInterface {
private static final long serialVersionUID = 1543383274516194973L;
@Tolerate
public DefaultUserQryWrapper(){}
@QueryCriteria(sqlOperator = SqlOperator.ge)
@OrderBy(direction = DirectionEnum.DESCENDING)
private Long id;
@QueryCriteria(sqlOperator = SqlOperator.in)
private Set grade;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@ApiModelProperty(value="出生日期(结束)")
@QueryCriteria(sqlOperator = SqlOperator.betweenAndTo,column = "birthday")
private Date birthdayTo;
@ApiModelProperty(value="部门Id")
@QueryCriteria(sqlOperator = SqlOperator.in)
private Set deptId;
@ApiModelProperty(value="用户名查询")
@QueryCriteria(sqlOperator = SqlOperator.like)
private String username;
@ApiModelProperty(value="姓名")
@QueryCriteria(sqlOperator = SqlOperator.like)
private String fullName;
}
- 单独抽取一个工具类,对
DefaultUserQryWrapper
处理,处理成jpa
的查询条件specification
:
@Slf4j
public class JPAQueryUtil {
/**
* 获取一个类及其父类的所有字段(属性,field)
*
* @param clazz invoked class
* @return fields include self and all parent */ private static List getAllFieldsWithRoot(Class> clazz) {
List fieldList = new ArrayList<>();
//get from this class self
Field[] dFields = clazz.getDeclaredFields();
if (dFields.length > 0)
fieldList.addAll(Arrays.asList(dFields));
Class> superClass = clazz.getSuperclass();
// if parent class is Object.class ,return ..
if (superClass == Object.class) return Arrays.asList(dFields);
// recursive to get from parent class
List superFields = getAllFieldsWithRoot(superClass);
if (!superFields.isEmpty()) {
superFields.stream().
filter(field -> !fieldList.contains(field)).
forEach(fieldList::add);
}
return fieldList;
}
//动态查询or连接
public static Specification toJPASpecificationWithOr(Q queryWrapper, Class entityCls) {
return toJPASpecificationWithLogic("or", queryWrapper, entityCls);
}
//动态查询and连接
public static Specification toJPASpecificationWithAnd(Q queryWrapper, Class entityCls) {
return toJPASpecificationWithLogic("and", queryWrapper, entityCls);
}
//logicType or/and
@SuppressWarnings({"rawtypes", "unchecked"})
public static Specification toJPASpecificationWithLogic(String logicType, Q queryWrapper, Class entityCls) {
return (root, criteriaQuery, criteriaBuilder) -> {
Class clazz = queryWrapper.getClass();
//获取查询类Query的所有字段,包括父类字段
List fields = getAllFieldsWithRoot(clazz);
List predicates = new ArrayList<>(fields.size());
Predicate predicate = null;
//wrapper中的column必须在实体类中存在,否则忽略!
List supportedFields = getAllFieldsWithRoot(entityCls);
Set supportFieldNames = supportedFields.stream().map(Field::getName).collect(Collectors.toSet());
if (CollUtil.isEmpty(supportFieldNames)) {
return predicate;
}
//init betweenAnd 用map存放between and 的字段对,from和to必须指向同一个column!
Map> betweenAndMap = new HashMap<>();
Map targetField;
for (Field field : fields) {
QueryCriteria queryCriteria = field.getAnnotation(QueryCriteria.class);
if (queryCriteria == null) {
continue;
}
String column = queryCriteria.column();
if (column.equals("")) {
column = field.getName();
}
if (!supportFieldNames.contains(column)) {
continue;
}
Path path = root.get(column);
if (queryCriteria.sqlOperator() == SqlOperator.isNull) {
predicates.add(criteriaBuilder.isNull(path));
continue;
}
if (queryCriteria.sqlOperator() == SqlOperator.isNotNull) {
predicates.add(criteriaBuilder.isNotNull(path));
continue;
}
field.setAccessible(true);
try {
// nullable
Object value = field.get(queryWrapper);
//如果值为null,注解未标注nullable,跳过
if (value == null && !queryCriteria.nullAble())
continue;
// can be empty
if (value != null && String.class.isAssignableFrom(value.getClass())) {
String s = (String) value;
if (s.equals("") && !queryCriteria.emptyAble()) {
continue;
}
}
switch (queryCriteria.sqlOperator()) {
case equal:
predicates.add(criteriaBuilder.equal(path, value));
break;
case like:
predicates.add(criteriaBuilder.like(path, "%" + value + "%"));
break;
case gt:
predicates.add(criteriaBuilder.gt(path, (Number) value));
break;
case lt:
predicates.add(criteriaBuilder.lt(path, (Number) value));
break;
case ge:
predicates.add(criteriaBuilder.ge(path, (Number) value));
break;
case le:
predicates.add(criteriaBuilder.le(path, (Number) value));
break;
case notEqual:
predicates.add(criteriaBuilder.notEqual(path, value));
break;
case notLike:
predicates.add(criteriaBuilder.notLike(path, "%" + value + "%"));
break;
case greaterThan:
predicates.add(criteriaBuilder.greaterThan(path, (Comparable) value));
break;
case greaterThanOrEqualTo:
predicates.add(criteriaBuilder.greaterThanOrEqualTo(path, (Comparable) value));
break;
case lessThan:
predicates.add(criteriaBuilder.lessThan(path, (Comparable) value));
break;
case lessThanOrEqualTo:
predicates.add(criteriaBuilder.lessThanOrEqualTo(path, (Comparable) value));
break;
case in://in 查询 wrpper中的字段值必须为Collection形式
if (!Collection.class.isAssignableFrom(field.getType())) {
log.warn("a search attribute named {} was ignored ! because for in Query,the value of search attribute must extend Collection", column);
break;
}
if (value instanceof Collection>) {
Collection> inValues = (Collection>) value;
CriteriaBuilder.In in = criteriaBuilder.in(path);
for (Object inValue : inValues) {
in.value(inValue);
}
predicates.add(in);
break;
}
if (null != value && value.getClass().isArray()) {
Object[] array = (Object[]) value;
CriteriaBuilder.In in = criteriaBuilder.in(path);
for (Object t : array) {
in.value(t);
}
predicates.add(in);
}
break;
case betweenAndFrom:
targetField = betweenAndMap.get(column);
if (ObjectUtil.isEmpty(targetField)) {
targetField = new HashMap<>();
betweenAndMap.put(column, targetField);
}
targetField.put("from", value);
break;
case betweenAndTo:
targetField = betweenAndMap.get(column);
if (ObjectUtil.isEmpty(targetField)) {
targetField = new HashMap<>();
betweenAndMap.put(column, targetField);
}
targetField.put("to", value);
break;
default:
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
//处理between and
List betweenAndPredicates = handleBetweenAndPredicates(betweenAndMap, root, criteriaBuilder);
predicates.addAll(betweenAndPredicates);
if (logicType == null || logicType.equals("") || logicType.equals("and")) {
predicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));//and连接
} else if (logicType.equals("or")) {
predicate = criteriaBuilder.or(predicates.toArray(new Predicate[0]));//or连接
}
return predicate;
};
}
@SuppressWarnings("unchecked")
private static List handleBetweenAndPredicates(Map> betweenAndMap, Root root, CriteriaBuilder criteriaBuilder) {
List betweenPredicates = new ArrayList<>();
if (!MapUtil.isEmpty(betweenAndMap)) {
for (Map.Entry> entry : betweenAndMap.entrySet()) {
String column = entry.getKey();
Map criteriaMap = entry.getValue();
Object from = criteriaMap.get("from");
Object to = criteriaMap.get("to");
if (ObjectUtil.isEmpty(from) && ObjectUtil.isEmpty(to)) {
continue;
}
//这里只检查日期或者数值类型
Class> valueType = ObjectUtil.isEmpty(from) ? to.getClass() : from.getClass();
if (Date.class.isAssignableFrom(valueType)) {
//如果日期查询中有from和to 有空的,用当前时间代替
Path path = root.get(column);
from = Optional.ofNullable(from).orElse(new Date());
to = Optional.ofNullable(to).orElse(new Date());
betweenPredicates.add(criteriaBuilder.between(
path, (Date) from, (Date) to));
continue;
}
//数值类型,如果有空的,则from = to
if (Integer.class.isAssignableFrom(valueType)) {
Path path = root.get(column);
betweenPredicates.add(criteriaBuilder.between(
path, Convert.toInt(from), Convert.toInt(to)));
continue;
}
log.warn("BetweenAnd Search Attribute was ignored,caused by unsupported field type {}", valueType);
}
}
return betweenPredicates;
}
}
这样使用:
Specification rawSpecif = JPAQueryUtil.toJPASpecificationWithAnd(useQry, UserPO.class);
Optional existedOne = userDORepository.findOne(rawSpecif);