在企业应用系统中,对于业务实体的管理,通过功能菜单进去通常默认是一个列表页面,如下图所示:
顶部为查询区域,放置1个或多个查询条件。
中间为操作按钮区域,如刷新、新增、删除等
主体区域为数据表格,显示具体记录,通常还有分页处理。
其中查询区域中的查询条件,实际业务需求有多种匹配规则,对于数据类型为字符串的属性,如相等、模糊匹配、以……开始、以……结束……;对于数据类型为数值类的属性,如等于、大于、小于、大于等于、小于等于……。
常规功能设计与实现,是将匹配规则固化到后端代码中,这么做灵活性比较差,当业务需求变化时,需要调整匹配规则,例如将相等调整为模糊匹配,则需要调整后端代码,并且编译、发布,比较繁琐,工作量也较大。
今天介绍一种将匹配规则交由前端控制的实现方案,整体实现思路如下:
前后端约定好数据格式为 左括号+规则编码+右括号+查询条件值,如(LK)abc,代表查询条件值是“abc”,查询规则为模糊匹配(like,简写为LK),然后传到后端,后端根据约定好的格式进行解析处理,根据匹配规则,生成对应的SQL语句。
这里选用小括号作为规则的边界值,主要在于大部分特殊符号,如中括号和花括号等,都会被url编码,首先可读性变差,其次,url编码后会增加url字符串的长度,而查询一般采用的是get请求的url长度是有上限的。
代码量不大,但是要处理好所有细节,会相对麻烦一些,先附上完整源码,后面再做进一步说明。
UI部分,基于element plus 的input组件进行封装,没什么好说的。
增加了几个属性props,方便使用方控制细节。其中type用来控制匹配规则,并且默认值设置为最常用的LK(模糊匹配)。
props: {
modelValue: {
type: String,
required: false,
default: ''
},
readonly: {
type: Boolean,
required: false,
default: false
},
type: {
type: String,
required: false,
default: 'LK'
},
placeholder: {
type: String,
required: false,
default: ''
}
}
监视modelValue的值变化, 触发handleValue方法。该方法将附加编码的值,进行处理,拿到文本值显示给用户。
handleValue() {
if (!this.modelValue) {
this.displayText = ''
} else {
// 根据约定的规则处理,获取显示内容
this.displayText = this.modelValue.substring(this.modelValue.indexOf(')') + 1)
}
}
下面则是最关键的处理,handleInput方法。在文本框中改变值时,先触发该函数,根据约定附加规则字符,并通过emit方法将值传给父组件。更新父组件中绑定的model中的数据,同时父组件的值改变,又会通过props机制传递给本组件的modelValue, 本组件通过watch值的变化,再把查询的规则字符去除掉,从而显示正常。
这种方式,一方面显示给用户的是正常文本,另一方面,父组件的查询控件绑定的值又是后台需要的特殊字符。
handleInput(value) {
if (value && value.length > 0) {
value = '(' + this.type + ')' + value
} else {
// 若为空,则直接清空,否则传给后台可能会出现只有规则字符串但没有值的情况,如(LK)
value = ''
}
// 将处理过,待查询特殊字符的值传给父组件绑定的数据
this.$emit('update:modelValue', value)
}
使用方调用,引入组件,然后跟使用el-input一样即可,默认匹配规则是模糊查询 ,可以附加type='EQ’等来指定其他规则。
输入查询条件,点击查询按钮,调用后端
可以看到,发起的请求,已经做过编码处理
http://localhost:4000/entityconfig/module/page?pageNum=1&pageSize=10&sort_field=orderNo&sort_sortType=ascending&name=(LK)%E7%B3%BB%E7%BB%9F&code=(LK)sys
注意:以上组件封装写法是基于vue3.0,与vue2.0相比,v-model属性的使用做了调整,详见官网说明:
https://v3-migration.vuejs.org/zh/breaking-changes/v-model.html
后端基于MybatisPlus组件的条件构造器,先附上完整源码。
package com.huayuan.platform.common.query;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.huayuan.platform.common.constant.DateConstant;
import com.huayuan.platform.common.exception.CommonException;
import com.huayuan.platform.common.exception.CustomException;
import com.huayuan.platform.common.utils.CommonUtil;
import com.huayuan.platform.common.vo.SortInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.beans.PropertyDescriptor;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 查询生成器
*
* @author wqliu
* @date 2023-03-06
*/
@Component
@Slf4j
public class QueryGenerator {
private static final String BEGIN = "BeginForQuery";
private static final String END = "EndForQuery";
private static final String STAR = "*";
private static final String COMMA = ",";
private static final String NOT_EQUAL = "!";
/**
* 构造查询条件构造器QueryWrapper实例
*/
public static <E, VO> QueryWrapper<E> generateQueryWrapper(Class<E> entityClass, VO vo, SortInfo sortInfo) {
QueryWrapper<E> queryWrapper = new QueryWrapper<E>();
build(queryWrapper, entityClass, vo, sortInfo);
return queryWrapper;
}
/**
* 构造查询条件构造器QueryWrapper实例
*/
public static <E, VO> QueryWrapper<E> generateQueryWrapper(Class<E> entityClass, VO vo) {
return generateQueryWrapper(entityClass, vo, null);
}
/**
* 构造查询对象
*/
private static <E, VO> void build(QueryWrapper<E> queryWrapper, Class<E> entityClass, VO vo, SortInfo sortInfo) {
// 获取实体属性
PropertyDescriptor[] origDescriptors = PropertyUtils.getPropertyDescriptors(entityClass);
// 遍历处理
for (int i = 0; i < origDescriptors.length; i++) {
String name = origDescriptors[i].getName();
Object value = null;
try {
value = PropertyUtils.getSimpleProperty(vo, name);
} catch (Exception e) {
// VO对象不一定包含Entity的每一个属性,此处找不到属于正常情况
}
// 单值处理
if (value != null) {
QueryRuleEnum rule = getRule(value);
String valueString = value.toString();
if (StringUtils.isNotBlank(valueString) && valueString.indexOf(')') >= 0) {
value = valueString.substring(valueString.indexOf(')') + 1);
}
addEasyQuery(queryWrapper, name, rule, value);
}
}
// 起止范围处理,如日期、数值
PropertyDescriptor[] voDescriptors = PropertyUtils.getPropertyDescriptors(vo.getClass());
List<PropertyDescriptor> scopeList = Arrays.stream(voDescriptors)
.filter(x -> x.getName().endsWith(BEGIN) || x.getName().endsWith(END)).collect(Collectors.toList());
for (PropertyDescriptor field : scopeList) {
String name = field.getName();
Object value = null;
try {
Object scopeValue = PropertyUtils.getSimpleProperty(vo, name);
if (name.endsWith(BEGIN)) {
addEasyQuery(queryWrapper, name.replace(BEGIN, ""), QueryRuleEnum.GE, scopeValue);
} else {
// 结束类型如果为日期时间类型,且时间部分为00:00:00,即只传入日期,则业务查询期望包含当天数据,系统自动附加23:59:59
if (field.getPropertyType() == LocalDateTime.class) {
if (scopeValue != null) {
LocalDateTime endValue = (LocalDateTime) scopeValue;
if (endValue.format(DateTimeFormatter.ISO_TIME).equals(DateConstant.BEGIN_OF_DAY)) {
scopeValue =
LocalDateTime.parse(endValue.format(DateTimeFormatter.ISO_DATE) + "T"
+ DateConstant.END_OF_DAY);
}
}
}
addEasyQuery(queryWrapper, name.replace(END, ""), QueryRuleEnum.LE, scopeValue);
}
} catch (Exception e) {
log.error("获取对象属性出错", e);
throw new CustomException(CommonException.PROPERTY_ACCESS_ERROR);
}
}
// 附加排序
if (sortInfo != null && StringUtils.isNotBlank(sortInfo.getField())) {
// 此处未使用注解,而是按照约定的规则,将驼峰命名转换为下划线,从而获取到数据库表字段名
String orderField = CommonUtil.camelToUnderline(sortInfo.getField());
if (sortInfo.getAscType()) {
queryWrapper.orderByAsc(orderField);
} else {
queryWrapper.orderByDesc(orderField);
}
}
}
/**
* 根据规则走不同的查询
*
* @param queryWrapper QueryWrapper
* @param name 字段名字
* @param rule 查询规则
* @param value 查询条件值
*/
private static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
if (value == null || rule == null || ObjectUtils.isEmpty(value)) {
return;
}
name = CommonUtil.camelToUnderline(name);
switch (rule) {
case GT:
queryWrapper.gt(name, value);
break;
case GE:
queryWrapper.ge(name, value);
break;
case LT:
queryWrapper.lt(name, value);
break;
case LE:
queryWrapper.le(name, value);
break;
case EQ:
queryWrapper.eq(name, value);
break;
case NE:
queryWrapper.ne(name, value);
break;
case IN:
if (value instanceof String) {
queryWrapper.in(name, (Object[]) value.toString().split(COMMA));
} else if (value instanceof String[]) {
queryWrapper.in(name, (Object[]) value);
} else {
queryWrapper.in(name, value);
}
break;
case LK:
queryWrapper.like(name, value);
break;
case LL:
queryWrapper.likeLeft(name, value);
break;
case RL:
queryWrapper.likeRight(name, value);
break;
default:
log.info("--查询规则未匹配到---");
break;
}
}
private static QueryRuleEnum getRule(Object value) {
// 避免空数据
if (value == null) {
return null;
}
String val = (value + "").trim();
if (val.length() == 0) {
return null;
}
String patternString = "\\((.*?)\\)";
// 创建 Pattern 对象
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(val);
if (matcher.find()) {
String ruleString = matcher.group(1);
return QueryRuleEnum.valueOf(ruleString);
}
// 对于数据字典,返回的是以逗号间隔的字符串,此种情况将操作符置为in
if (StringUtils.contains(val, COMMA)) {
return QueryRuleEnum.IN;
}
// 未找到,默认返回相等
return QueryRuleEnum.EQ;
}
}
首先是匹配规则的解析,通过正则表达式解析,获取到小括号包含的匹配规则编码,然后转换成枚举类型。
private static QueryRuleEnum getRule(Object value) {
// 避免空数据
if (value == null) {
return null;
}
String val = (value + "").trim();
if (val.length() == 0) {
return null;
}
String patternString = "\\((.*?)\\)";
// 创建 Pattern 对象
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(val);
if (matcher.find()) {
String ruleString = matcher.group(1);
return QueryRuleEnum.valueOf(ruleString);
}
// 对于数据字典,返回的是以逗号间隔的字符串,此种情况将操作符置为in
if (StringUtils.contains(val, COMMA)) {
return QueryRuleEnum.IN;
}
// 未找到,默认返回相等
return QueryRuleEnum.EQ;
}
然后是根据匹配规则,使用MyBatisPlus的条件构造器,构造查询条件
/**
* 根据规则走不同的查询
*
* @param queryWrapper QueryWrapper
* @param name 字段名字
* @param rule 查询规则
* @param value 查询条件值
*/
private static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
if (value == null || rule == null || ObjectUtils.isEmpty(value)) {
return;
}
name = CommonUtil.camelToUnderline(name);
switch (rule) {
case GT:
queryWrapper.gt(name, value);
break;
case GE:
queryWrapper.ge(name, value);
break;
case LT:
queryWrapper.lt(name, value);
break;
case LE:
queryWrapper.le(name, value);
break;
case EQ:
queryWrapper.eq(name, value);
break;
case NE:
queryWrapper.ne(name, value);
break;
case IN:
if (value instanceof String) {
queryWrapper.in(name, (Object[]) value.toString().split(COMMA));
} else if (value instanceof String[]) {
queryWrapper.in(name, (Object[]) value);
} else {
queryWrapper.in(name, value);
}
break;
case LK:
queryWrapper.like(name, value);
break;
case LL:
queryWrapper.likeLeft(name, value);
break;
case RL:
queryWrapper.likeRight(name, value);
break;
default:
log.info("--查询规则未匹配到---");
break;
}
}
最后是build方法,负责构建完整的查询条件,如只考虑上文说的单字符串类型的查询条件,其实很简单。上面代码中方法比较复杂,是考虑多种情况,不仅仅是简单查询,还有范围查询。也不仅仅是字符串,还包括日期和数值类型的查询处理。
按照上述技术方案和实现方式,最终我们实现了可灵活设置匹配规则的前端查询条件组件封装,以及后端自动解析查询条件,构造查询sql的工作。在列表页面中,前端开发人员,可以方便的控制查询条件,给查询条件设置或修改匹配规则。如果进一步配合可配置的代码生成器或低代码开发平台,大幅提升开发效率。