news 2026/4/30 13:39:10

自定义查询条件:业务代码一行不改,框架层透明注入WHERE

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自定义查询条件:业务代码一行不改,框架层透明注入WHERE

自定义查询条件:业务代码一行不改,框架层透明注入WHERE

非科班野生程序员,深耕政务信息化20年。列表查询页面加"自定义查询"功能——用户选字段、选运算符、填值,框架自动拼WHERE条件注入SQL。13个列表页面加了这个功能,业务Controller一行代码没改。这篇拆解这个透明注入的设计。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。


背景

政务系统的列表页面,上线后用户总会有新的查询需求:“能不能按缴费地筛选?”“能不能查某个日期范围的?”“能不能模糊搜姓名?”

每来一个需求,开发人员就要:改SQL → 加参数 → 改前端 → 测试 → 上线。频繁得很。而且大部分改动就是加一个WHERE条件,技术含量为零,纯粹是体力活。

能不能让用户自己选字段、选条件、填值,不用改代码?


两种入口

列表页面的列信息从哪来?两种来源,两个入口:

入口一:从Grid的列定义来——searchsetting(gridId, onComplete)

列表页面已经有Grid组件,Grid的LayoutManager管理了所有列的定义(字段名、中文标题、数据类型)。直接从这里提取:

functionsearchsetting(id,oncompeate){varlayout=browise.byId(id).getManager("LayoutManager");vararr=layout.getCells();vararr2=[];for(vari=0;i<arr.length;i++){arr2[i]={name:arr[i].name,label:arr[i].label,dataType:arr[i].dataType};}varcols=newbrowise.ds.DataStore('cols',arr2);// 打开对话框,把列信息传进去DialogUtil.showDialog({title:'自定义查询',url:"/pages/system/searchset.jsp",dialogData:{ds:cols},onComplete:oncompeate});}

优点:不需要调后端,列信息前端已有。大多数列表页面用这个。

入口二:从数据库系统表来——searchsettingByTab(tableName, onComplete)

有些场景需要查的列比Grid显示的多。比如Grid只显示了5列,但用户想按数据库表的第8列筛选。这时候从数据库系统表查列信息:

functionsearchsettingByTab(tableName,oncompeate){DialogUtil.showDialog({title:'自定义查询',url:"/pages/system/searchsetByTab.jsp",dialogData:{tname:tableName},onComplete:oncompeate});}

后端QueryCloumn2方法从Oracle的user_tab_columnsuser_col_comments(或SQL Server的sys.columnssys.extended_properties)查列名、数据类型、中文注释,返回给前端。

两个入口,同一个对话框UI,只是列数据的来源不同。


对话框UI

弹出一个窗口,里面是一个可编辑的Grid,4列:

列名类型说明
字段名下拉框从传入的列信息填充,显示中文标题,存字段名
关系符下拉框=等于、like包含、>=大于等于、<=小于等于、>大于、<小于
文本框用户自由输入
数据类型只读选字段名后自动填入

用户可以添加多行条件,每行一个条件,全部用AND连接。点"确定"后,对话框把所有行数据返回给回调函数。

选字段名时自动填数据类型——cname()函数在下拉框变化时触发,从列信息DataStore中查找匹配的字段,把dataType填到第四列。这个细节很关键——后端拼SQL时需要根据数据类型决定怎么处理值。


关键设计:morewheres约定

回调函数收到对话框返回的条件行后,创建一个**名字固定为morewheres**的DataStore:

functionComplete2(value){if(!value)return;vararr=[];for(vari=0;i<value.length;i++){arr[i]={name:value[i].name,// 字段名val:value[i].val,// 值nation:value[i].nation,// 运算符datatype:value[i].dataType// 数据类型};}varmore=newbrowise.ds.DataStore('morewheres',arr);// 名字必须是morewheressearch(more);}

morewheres这个名字是前后端之间的约定契约。前端必须用这个名字,后端按这个名字取。


透明注入:三层接力

这是整个设计最巧妙的部分。自定义条件从用户输入到最终生效,经过三层接力,业务Controller全程无感知

第一层:route.java——拦截并存储

框架的统一入口Servletroute.java,在把请求分发给业务Controller之前,先检查请求体里有没有叫morewheres的DataStore:

// route.javaDataStoremore=dc.getBody().getDatastore("morewheres");if(more!=null){dc.getBody().removeStore("morewheres");// 从请求体中移除context.setMore(more);// 存到线程上下文}

三步:取出来、从请求里删掉、存到ThreadLocal上下文

删掉的原因是——业务Controller不应该看到这个东西。它只管自己的查询逻辑,自定义条件是"横切关注点",由框架统一处理。

第二层:AppContext——ThreadLocal透传

context.setMore(more)存到了AppContextContainer的 ThreadLocal 里。这个上下文和当前请求绑定,请求结束就清理。

业务Controller执行查询时,它调的是标准的DBUtil.getDao(),传的是 RowBounds 分页参数。它不知道、也不需要知道自定义条件的存在。

第三层:PaginationInterceptor——SQL注入

MyBatis的分页拦截器PaginationInterceptor拦截所有带 RowBounds 的查询,调用方言类的getLimitString()方法生成分页SQL。

在SQL Server方言MsSqlDialect.getLimitString()里,分页SQL组装到一半时,检查上下文里有没有自定义条件:

// MsSqlDialect.javaDataStoremore=AppContextContainer.getAppContext().getMore();if(more!=null&&more.getRowset().getPrimary().size()>0){pagingSelect.append(" select * from (");}pagingSelect.append(sql);// 原始SQLif(more!=null&&more.getRowset().getPrimary().size()>0){Stringmorewhere=getMorewhere(more);pagingSelect.append(") as cte333");pagingSelect.append(" where ");pagingSelect.append(morewhere);}

最终生成的SQL长这样:

-- 原始SQL:select psn_name, amount from t_payment-- 加上自定义条件(psn_name like '%张%')和分页后:select*from(selectcte1.*,row_number()over(orderbypsn_nameasc)rownum_from(select*from(selectpsn_name,amountfromt_payment)ascte333wherepsn_namelike'%张%')ascte1)asctewhererownum_<=50andrownum_>0

原始SQL被包了一层子查询,WHERE条件加在外层。这个"包装"手法确保自定义条件不会和原始SQL的WHERE子句冲突。

getMorewhere()——按数据类型拼值

privateStringgetMorewhere(DataStoremore){Stringwheres="";for(inti=0;i<more.getRowset().getPrimary().size();i++){if(i>0)wheres+=" and ";Rowrow=more.getRowset().getrow(i);Stringname=row.getItemStringValue("name");// 字段名Stringnation=row.getItemStringValue("nation");// 运算符Stringval=row.getItemStringValue("val");// 值Stringdatatype=row.getItemStringValue("datatype");wheres+=name+" "+nation;if("number".equals(datatype)||"2".equals(datatype)){// 数字类型:convert包装if(val.indexOf(".")>0)val=" convert(decimal(15,4),'"+val+"') ";elseval=" convert(int,'"+val+"') ";}elseif("date".equals(datatype)||"4".equals(datatype)){// 日期类型:convert(datetime,...)val=" convert(datetime,'"+val+"') ";}else{// 字符串类型if("like".equals(nation))val=" '%"+val+"%'";elseval=" '"+val+"'";}wheres+=val;}returnwheres;}

三种数据类型三种处理:

  • 字符串:直接加引号,like运算符自动加%通配符
  • 数字:用convert(int,...)convert(decimal(15,4),...)包装,防止类型不匹配
  • 日期:用convert(datetime,...)包装,用户输入2024-01-01这种格式就行

完整数据流

用户点"自定义查询"按钮 ↓ searchsetting(gridId, Complete2) 或 searchsettingByTab(tableName, Complete2) ↓ 弹出对话框 → 用户选字段、运算符、填值 → 点确定 ↓ Complete2回调 → 创建名为'morewheres'的DataStore → search(more) ↓ HTTP POST请求,morewheres DataStore在JSON请求体中 ↓ route.java → 拦截morewheres → 存到ThreadLocal → 从请求体删除 ↓ 业务Controller.select() → 正常执行MyBatis查询(不知道morewheres的存在) ↓ PaginationInterceptor → MsSqlDialect.getLimitString() ↓ 从ThreadLocal取出morewheres → 拼WHERE条件 → 包裹在分页SQL中 ↓ SQL执行 → 结果返回 → Grid显示

给一个列表页加"自定义查询"需要改几行代码?

前端3行,后端0行。

前端改动(以lxList.jsp为例):

// 1. 加一个按钮<button onclick="searchsetting('grid', Complete2)">自定义</button>// 2. 加回调函数functionComplete2(value){if(!value)return;vararr=[];for(vari=0;i<value.length;i++){arr[i]={name:value[i].name,val:value[i].val,nation:value[i].nation,datatype:value[i].dataType};}varmore=newbrowise.ds.DataStore('morewheres',arr);search(more);}

后端:不需要改任何东西。业务Controller照常写查询,分页照常用RowBounds,框架自动处理。


诚实地说:不足之处

  1. SQL注入风险getMorewhere()里值是直接拼接进SQL的,没有参数化。政务内网环境下风险可控(用户是内部人员),但不符合安全规范。改进方向是改用绑定变量。

  2. Oracle方言没实现OracleDialect没有处理morewheres,只做了分页。如果部署到Oracle环境,自定义条件会静默失效。当时项目跑在SQL Server上,就没做。

  3. 只支持AND:多条件之间只能AND连接,不支持OR、括号分组。政务场景下大部分查询是AND条件叠加,够用了。如果需要OR,那就不叫"自定义查询"了,那叫"高级查询"——复杂度上升一个数量级,不值得。

  4. 条件不保存:用户设的条件用完就没了,下次还得重新填。如果要保存,需要一个用户偏好表,当时没做——用户没提这个需求。


决策原则

横切关注点不应该出现在业务代码里。

自定义查询条件是一个典型的横切关注点——每个列表页面都可能需要,但每个页面的处理逻辑完全一样。如果让每个业务Controller自己处理morewheres,13个页面就是13份重复代码。

把拦截、存储、注入全部做在框架管道里(route.java → AppContext → PaginationInterceptor),业务代码完全无感知。加一个功能只需要前端加一个按钮和回调,后端零改动。

这个思路和SM4加解密做在DBUtil管道里、审计日志做在commit()里、慢SQL检测做在AOP里——是同一个设计哲学:管道做的事,业务不该知道。


你的项目里自定义查询是怎么做的?是每个页面自己写还是框架统一处理?欢迎评论区聊聊。


作者:许彰午| 非科班野生程序员,深耕政务信息化20年

标签:#Java #MyBatis #动态查询 #分页 #低代码 #政务信息化

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/30 13:36:40

3分钟搞定:Windows电脑安装安卓应用的终极免费方案

3分钟搞定&#xff1a;Windows电脑安装安卓应用的终极免费方案 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 还在为在电脑上运行安卓应用而烦恼吗&#xff1f;传统模…

作者头像 李华
网站建设 2026/4/30 13:28:26

MATLAB新手也能搞定:用代码画多模光纤里的‘光斑’(附完整源码)

MATLAB实战&#xff1a;从零绘制多模光纤中的光斑图景 当你第一次在显微镜下观察多模光纤输出的光斑时&#xff0c;那些复杂而美丽的图案是否让你好奇它们是如何形成的&#xff1f;作为光学或通信领域的学习者&#xff0c;掌握用代码再现这些物理现象的能力&#xff0c;就像获…

作者头像 李华
网站建设 2026/4/30 13:25:11

2025年终极解决方案:如何免费突破8大网盘限速,实现全速下载?

2025年终极解决方案&#xff1a;如何免费突破8大网盘限速&#xff0c;实现全速下载&#xff1f; 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 &#xff0c;支持 百度网盘 / 阿…

作者头像 李华