通用查询

前言

通常情况下,各类系统均不可避免地涉及查询功能。传统的做法往往是前端传递实体的属性,后端则基于这些属性作为筛选条件来执行查询。这种方法经常借助MybatisPlus的QueryWrapper来动态构建查询条件。然而,当查询需求不够明确或经常变动时,这种方式可能显得不够灵活和高效。为了应对这一挑战,本快速开发框架在查询机制上采用了更为独特的方法。我们定义了一套通用的查询规范,前端可以根据实际需求灵活调整查询条件,从而极大地提升了查询的便捷性和适应性。

简单查询分析

主键查询

select * from sys_role where id = ?

等于

select * from sys_role where name = ?

不等于

select * from sys_role where name <> ?

大于

select * from sys_role where id > ?

大于等于

select * from sys_role where id >= ?

小于

select * from sys_role where id < ?

小于等于

select * from sys_role where id <= ?

区间范围

select * from sys_role where id between ? and ?

非区间范围

select * from sys_role where id not between ? and ?

模糊(全/左/右)

select * from sys_role where name like ?

值集合

select * from sys_role where id in (?,?,?)

非值集合

select * from sys_role where id not in (?,?,?)

组合查询(and)

select * from sys_role where name = ? and id between ? and ?

操作说明

操作名说明
EQ等于=
NE不等于<>
GT大于>
GE大于等于>=
LT小于<
LE小于等于<=
BTbetween 值1 and 值2
LIKElike '%值%'
NLIKEnot like '%值%'
LLIKElike '%abc'
RLIKElike 'abc%'
INin(值1,值2)
NINnot in(值1,值2)

入参设计

{
	"pageNum": 1,
	"pageSize": 15,
  "m_LIKE_name": "admin",
  "m_BT_createTime": ["2020-01-01","2020-06-06"]
}

无表别名:m_EQ_name 有表别名:m_t_EQ_name

构建条件代码

mldong-framewark/mldong-base/src/main/java/com/mldong/ase/PageParam.java

package com.mldong.base;


import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mldong.web.QueryParamHolder;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * 分页查询基类
 * @author mldong
 *
 * @param <T>
 */
@Data
public class PageParam<T> {
	/**
	 * 每几页
	 */
	@ApiModelProperty(value="每几页")
	private Integer pageNum;
	/**
	 * 每页大小
	 */
	@ApiModelProperty(value="每页大小")
    private Integer pageSize;
	@ApiModelProperty(value="关键字",hidden = true)
	private String keywords;
	@ApiModelProperty(value = "关键字对应的列名,多个使用英文逗号分割", hidden = true)
	private String searchKeys;
	@ApiModelProperty(value="排序字段")
	private String orderBy;
	@ApiModelProperty(value="排序类型,业务中自行转换成orderBy")
	private Integer orderType;
	@ApiModelProperty(value="是否查询总条数")
	private YesNoEnum isCount;
	@ApiModelProperty(value = "查询模式",hidden = true)
	private Integer includeType;
	@ApiModelProperty(value="或查询-下拉列表编辑模式专用",hidden = true)
	private List<String> includeIds;
	@ApiModelProperty(value="数据范围就是组织机构id集合",hidden = true)
	private List<Long> dataScope;
	@ApiModelProperty(value="扩展属性名,多个英文逗号分割")
	private String extFieldNames;
	final static String REG_EXP = "(^_([a-zA-Z0-9\\s]_?)*$)|(^[a-zA-Z\\.](_?[a-zA-Z0-9\\.\\s])*_?$)";
	@ApiModelProperty(value="过滤类型")
	private Integer filterType;
	@ApiModelProperty(value="过滤类型集合")
	private List<Integer> filterTypes;
	@ApiModelProperty(value = "要排除的查询条件,多个使用英语逗号分割。例:(m_EQ_id,m_LIKE_name)",hidden = true)
	private String excludeWheres;
	/**
	 * 构建mybatis-plus分页对象
	 * @return
	 */
	public IPage buildMpPage() {
		if(pageNum==null) {
			this.pageNum = 1;
		}
		if(pageSize == null) {
			this.pageSize = 10;
		}
		IPage page = new Page(getPageNum(),getPageSize(), !YesNoEnum.NO.equals(isCount));
		return page;
	}

	/**
	 * 构建查询条件
	 * @return 默认从请求体中获取
	 */
	public QueryWrapper buildQueryWrapper() {
		Dict dict = QueryParamHolder.me();
		return buildQueryWrapper(dict);
	}

	/**
	 * 构建查询条件
	 * @param dict 入参
	 * @return
	 */
	public QueryWrapper buildQueryWrapper(Dict dict) {
		QueryWrapper queryWrapper = Wrappers.query();
		// 获取所有属性m_{type}_{column}
		// 有表别名时m_{alias}_{type}_{column}
		Field fields [] = ReflectUtil.getFields(this.getClass());
		Arrays.stream(fields).forEach(field->{
			// 实体类中的值可覆盖请求正文中的值-即实体类的值优先,方便在服务端调整值后再查询
			dict.put(field.getName(),ReflectUtil.getFieldValue(this, field.getName()));
		});
		String keys [] = dict.keySet().toArray(new String[]{});
		List<String> excludeWhereList = new ArrayList<>();
		if(StrUtil.isNotEmpty(excludeWheres)) {
			excludeWhereList.addAll(CollectionUtil.toList(excludeWheres.split(",")));
		}
		for (int i = 0; i < keys.length ; i++) {
			//Field field = fields[i];
			String fieldName = keys[i];
			if(excludeWhereList.contains(fieldName))continue;
			if(!fieldName.startsWith("m_")) continue;
			String arr [] = fieldName.split("_");
			if(arr.length !=3 && arr.length!=4) continue;
			String type = "";
			String column = "";
			if(arr.length == 3) {
				type = arr[1];
				column = StrUtil.toUnderlineCase(arr[2]);
			} else {
				type = arr[2];
				column = arr[1] + "." + StrUtil.toUnderlineCase(arr[3]);
			}
			// Object fieldValue = ReflectUtil.getFieldValue(this, fieldName);
			Object fieldValue = dict.get(fieldName);
			if(ObjectUtil.isEmpty(fieldValue)) continue;

			switch (type){
				case "EQ":
					queryWrapper.eq(column, fieldValue);
					break;
				case "NE":
					queryWrapper.ne(column, fieldValue);
					break;
				case "GT":
					queryWrapper.gt(column, fieldValue);
					break;
				case "GE":
					queryWrapper.ge(column, fieldValue);
					break;
				case "LT":
					queryWrapper.lt(column, fieldValue);
					break;
				case "LE":
					queryWrapper.le(column, fieldValue);
					break;
				case "LIKE":
					queryWrapper.like(column, fieldValue);
					break;
				case "NLIKE":
					queryWrapper.notLike(column, fieldValue);
					break;
				case "LLIKE":
					queryWrapper.likeLeft(column, fieldValue);
					break;
				case "RLIKE":
					queryWrapper.likeRight(column, fieldValue);
					break;
				case "BT":
					if(fieldValue instanceof List) {
						List btValue = Convert.toList(fieldValue);
						if(btValue.size()==2) {
							Object btValue1 = btValue.get(0);
							Object btValue2 = btValue.get(1);
							queryWrapper.between(column, btValue1, btValue2);
						}
					} else if(fieldValue instanceof String) {
						String btArr [] = ((String) fieldValue).split(",");
						Object btValue1 = btArr[0];
						Object btValue2 = btArr[1];
						queryWrapper.between(column, btValue1, btValue2);
					}
					break;
				case "IN":
					if(fieldValue instanceof List) {
						List inValue = Convert.toList(fieldValue);
						queryWrapper.in(column, inValue);
					} else if(fieldValue instanceof String) {
						String btArr [] = ((String) fieldValue).split(",");
						queryWrapper.in(column, CollectionUtil.toList(btArr));
					}
					break;
				case "NIN":
					if(fieldValue instanceof List) {
						List inValue = Convert.toList(fieldValue);
						queryWrapper.notIn(column, inValue);
					} else if(fieldValue instanceof String) {
						String btArr [] = ((String) fieldValue).split(",");
						queryWrapper.notIn(column, CollectionUtil.toList(btArr));
					}
					break;
			}
		}
		// 关键字处理
		if(StrUtil.isNotEmpty(keywords) && StrUtil.isNotEmpty(searchKeys)) {
			queryWrapper.and(new Consumer<QueryWrapper<?>>() {
				@Override
				public void accept(QueryWrapper<?> tQueryWrapper) {
					Arrays.stream(searchKeys.split(",")).forEach(v -> tQueryWrapper.or().like(CharSequenceUtil.toUnderlineCase(v), keywords));
				}
			});
		}
		// 构建排序
		// id desc
		// id asc
		// id desc,code asc
		// id,code asc
		if(StrUtil.isEmpty(orderBy)) return queryWrapper;
		String arr [] = orderBy.split(",");
		for(String s:arr) {
			String orderByList [] = s.split("\\s+");
			if (orderByList.length==1) {
				String column = StrUtil.toUnderlineCase(orderByList[0]);
				if (column.matches(REG_EXP)) {
					queryWrapper.orderByAsc(column);
				}
			} else {
				String column = StrUtil.toUnderlineCase(orderByList[0]);
				if (column.matches(REG_EXP)) {
					if("desc".equalsIgnoreCase(orderByList[1]) || "descend".equalsIgnoreCase(orderByList[1])) {
						queryWrapper.orderByDesc(column);
					} else {
						queryWrapper.orderByAsc(column);
					}
				}
			}
		}
		return queryWrapper;
	}
	public int getPageSize() {
		// 每页大小大于0,小于10000
		if(pageSize == null) {
			pageSize = 10;
		}
		return pageSize<=0?10:pageSize>10000?10000:pageSize;
	}
	private static Dict QUERY_TYPE_MAP = Dict.of("EQ","等于","LIKE","模糊","IN","包含","BT","区间","NE","不等于","GT","大于","GE"
			,"大于等于","LT","小于","LE","小于等于","LLIKE","左模糊","RLIKE","右模糊","NIN","不包含") ;
	/**
	 * 生成高级搜索查询条件
	 * @return
	 */
	public Map<String,List<Dict>> buildQuerySchema() {
		return Arrays.stream(ReflectUtil.getFields(this.getClass())).filter(item->{
			return item.getName().startsWith("m_");
		}).map(field -> fieldToQuerySchema(field)).collect(Collectors.groupingBy(item->{
			String remark = item.getStr("remark");
			String queryFieldName = item.getStr("queryFieldName");
			String arr [] = queryFieldName.split("_");
			String queryType = "EQ";
			if(arr.length == 3) {
				queryType = arr[1];
			} else {
				queryType = arr[2];
			}
			item.set("remark",QUERY_TYPE_MAP.getStr(queryType));
			return remark;
		}));
	}

	/**
	 * 搜索字段转成元数据
	 * @param field
	 * @return
	 */
	private Dict fieldToQuerySchema(Field field) {
		Dict dict = Dict.create();
		ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
		String remark = apiModelProperty.value();
		dict.put("queryFieldName",field.getName());
		dict.put("remark",remark);
		Dict ext  = Dict.create();
		for (int i = 0; i < apiModelProperty.extensions().length; i++) {
			for (int i1 = 0; i1 < apiModelProperty.extensions()[i].properties().length; i1++) {
				ext.put(apiModelProperty.extensions()[i].properties()[i1].name(),apiModelProperty.extensions()[i].properties()[i1].value());
			}
		}
		dict.put("ext",ext);
		return dict;
	}
}

xml自定义查询样例

RoleMapper.java增加方法

package com.mldong.modules.sys.mapper;

import com.mldong.modules.sys.entity.Role;
import com.mldong.modules.sys.vo.RoleVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * <p>
 * 角色 Mapper 接口
 * </p>
 *
 * @author mldong
 * @since 2023-09-20
 */
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
    List<RoleVO> selectCustom(IPage<RoleVO> page, @Param(Constants.WRAPPER) Wrapper<Role> wrapper);
    RoleVO findById(@Param("id") Long id);
}

增加mapping/RoleMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mldong.modules.sys.mapper.RoleMapper">

    <select id="selectCustom" resultType="com.mldong.modules.sys.vo.RoleVO">
        select * from sys_role t ${ew.customSqlSegment}
    </select>
    <select id="findById" resultType="com.mldong.modules.sys.vo.RoleVO">
        select * from sys_role t where t.id = #{id}
    </select>
</mapper>

关于关键字查询

入参设计

单字段模糊查询 keywords=xx searchKeys=userName

select * from sys_user where user_name like ?

多字段模糊查询 keywords=xx searchKeys=userName,realName

select * from sys_user where  (user_name LIKE ? OR real_name LIKE ?) and is_deleted=0
// 关键字处理
if(StrUtil.isNotEmpty(keywords) && StrUtil.isNotEmpty(searchKeys)) {
    queryWrapper.and(new Consumer<QueryWrapper<?>>() {
        @Override
        public void accept(QueryWrapper<?> tQueryWrapper) {
            Arrays.stream(searchKeys.split(",")).forEach(v -> tQueryWrapper.or().like(CharSequenceUtil.toUnderlineCase(v), keywords));
        }
    });
}

代码片段

同PageParam

关于排序

入参设计

单个正序

orderBy="id asc"

select * from sys_role where id asc

单个倒序

orderBy="id desc"

select * from sys_role where id desc

多个正序

orderBy="id asc,code asc"

select * from sys_role where id asc, code asc

多个倒序

orderBy="id desc,code desc"

select * from sys_role where id desc, code desc

正序倒序都存在

orderBy="id desc,code asc"

select * from sys_role where id desc, code asc

有表别名

orderBy="r.id desc,r.code asc"

select * from sys_role r where r.id desc, r.code asc

代码片段

同PageParam

// 构建排序
// id desc
// id asc
// id desc,code asc
// id,code asc
if(StrUtil.isEmpty(orderBy)) return queryWrapper;
String arr [] = orderBy.split(",");
for(String s:arr) {
    String orderByList [] = s.split("\\s+");
    if (orderByList.length==1) {
        String column = StrUtil.toUnderlineCase(orderByList[0]);
        if (column.matches(REG_EXP)) {
            queryWrapper.orderByAsc(column);
        }
    } else {
        String column = StrUtil.toUnderlineCase(orderByList[0]);
        if (column.matches(REG_EXP)) {
            if("desc".equalsIgnoreCase(orderByList[1]) || "descend".equalsIgnoreCase(orderByList[1])) {
                queryWrapper.orderByDesc(column);
            } else {
                queryWrapper.orderByAsc(column);
            }
        }
    }
}