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

文章介绍了将查询条件的匹配规则从前端控制并传递给后端解析处理的方案,通过在前端对用户输入添加规则编码,然后在后端根据约定的格式解析生成SQL。这种方式提高了系统的灵活性,减少了因业务需求变化导致的代码调整。文章提供了基于Vue3的组件代码示例和MybatisPlus的后端处理代码,展示了如何构建和解析查询条件。
摘要由CSDN通过智能技术生成

背景

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

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

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

实现方案

今天介绍一种将匹配规则交由前端控制的实现方案,整体实现思路如下:
前后端约定好数据格式为 左括号+规则编码+右括号+查询条件值,如(LK)abc,代表查询条件值是“abc”,查询规则为模糊匹配(like,简写为LK),然后传到后端,后端根据约定好的格式进行解析处理,根据匹配规则,生成对应的SQL语句。
这里选用小括号作为规则的边界值,主要在于大部分特殊符号,如中括号和花括号等,都会被url编码,首先可读性变差,其次,url编码后会增加url字符串的长度,而查询一般采用的是get请求的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 {
        // 若为空,则直接清空,否则传给后台可能会出现只有规则字符串但没有值的情况,如(LK)
        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 {
        // 若为空,则直接清空,否则传给后台可能会出现只有规则字符串但没有值的情况,如(LK)
        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的工作。在列表页面中,前端开发人员,可以方便的控制查询条件,给查询条件设置或修改匹配规则。如果进一步配合可配置的代码生成器或低代码开发平台,大幅提升开发效率。

开发平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学海无涯,行者无疆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值