灵活设置匹配规则的查询条件技术方案与实现

背景

在企业应用系统中,对于业务实体的管理,通过功能菜单进去通常默认是一个列表页面,如下图所示:
image.png
顶部为查询区域,放置1个或多个查询条件。
中间为操作按钮去,如刷新、新增、删除等
主体区域为数据表格,显示具体记录,通常还有分页处理。

其中查询区域中的查询条件,实际业务需求有多种匹配规则,对于数据类型为字符串的属性,如完全相同、模糊查匹配、以……开始、以……结束……;对于数据类型为数值类的属性,如等于、大于、小于、大于等于、小于等于……。

常规功能设计与实现,是将匹配规则固化到后端代码中,这么做灵活性比较差,当业务需求变化时,需要调整匹配规则,例如将完全相同调整为模糊匹配,则需要调整后端代码,并且编译、发布,比较繁琐,工作量也较大。

实现方案

今天介绍一种将匹配规则交由前端控制的实现方案,整体实现思路如下:
前后端约定好数据格式为 左括号+规则编码+右括号+查询条件值,如(LK)abc,代表查询条件值是“abc”,查询规则为模糊匹配(like,简写为LK),然后传到后端,后端根据约定好的格式进行解析处理,根据匹配规则,生成对应的SQL语句。
这里选用小括号作为规则的边界值,主要在于大部分特殊符号,如中括号和花括号等,都会被url编码,而查询一般采用的是get请求,url编码后会增加url字符串的长度,可读性会变差一些。

前端处理

代码量不大,但是要处理好所有细节,会相对麻烦一些,先附上完整源码,后面再做进一步说明。

<template>
  <el-input
    v-model="displayText"
    :placeholder="placeholder"
    :disabled="readonly"
    @input="handleInput"
  />
</template>


<script>
export default {
  name: 'QueryText',
  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: ''
    }
  },
  data() {
    return {
      // 显示内容
      displayText: ''
    }
  },
  watch: {
    modelValue: {
      immediate: true,
      handler: 'handleValue'
    }
  },
  methods: {   
    handleValue() {
      if (!this.modelValue) {
        this.displayText = ''
      } else {
        // 根据约定的规则处理,获取显示内容
        this.displayText = this.modelValue.substring(this.modelValue.indexOf(')') + 1)
      }
    },   
    handleInput(value) {
      if (value && value.length > 0) {
        value = '(' + this.type + ')' + value
      } else {
        // 若为空,则直接清空,否则传给后台可能会出现只有查询字符没有内容的情况,如*,>=等
        value = ''
      }
      // 将处理过,待查询特殊字符的值传给父组件绑定的数据
      this.$emit('update:modelValue', value)
    }
  }
}
</script>


<style></style>

UI部分,基于element plus 的input组件进行封装,没什么好说的。

<template>
  <el-input
    v-model="displayText"
    :placeholder="placeholder"
    :disabled="readonly"
    @input="handleInput"
  />
</template>

增加了几个属性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 {
        // 若为空,则直接清空,否则传给后台可能会出现只有查询字符没有内容的情况,如*,>=等
        value = ''
      }
      // 将处理过,待查询特殊字符的值传给父组件绑定的数据
      this.$emit('update:modelValue', value)
    }

使用方调用,引入组件,然后跟使用el-input一样即可,默认匹配规则是模糊查询 ,可以附加type='EQ’等来指定其他规则。

<el-form :inline="true" :model="queryCondition" label-width="80px" @keyup.enter="query">
      <!--查询条件区 -->
      <el-form-item label="名称">
        <QueryText v-model="queryCondition.name" class="form-item" />
      </el-form-item>
      <el-form-item label="编码">
        <QueryText v-model="queryCondition.code" class="form-item" />
      </el-form-item>
      <el-form-item style="float: right">
        <QueryButton :page-code="pageCode" />
      </el-form-item>
      <div class="clearfix"></div>
</el-form>

输入查询条件,点击查询按钮,调用后端
image.png
可以看到,发起的请求,已经做过编码处理
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的工作。在列表页面中,前端开发人员,可以方便的添加查询条件,给查询条件设置或修改匹配规则。如果进一步配合可配置的代码生成器或低代码开发平台,则能大幅提升开发效率。

猜你喜欢

转载自blog.csdn.net/seawaving/article/details/129887992