自定义查询条件业务代码一行不改框架层透明注入WHERE非科班野生程序员深耕政务信息化20年。列表查询页面加自定义查询功能——用户选字段、选运算符、填值框架自动拼WHERE条件注入SQL。13个列表页面加了这个功能业务Controller一行代码没改。这篇拆解这个透明注入的设计。最后感谢豆包、智谱、OpenCode决策是我做的代码是我搓的文字是他们总结的。背景政务系统的列表页面上线后用户总会有新的查询需求“能不能按缴费地筛选”“能不能查某个日期范围的”“能不能模糊搜姓名”每来一个需求开发人员就要改SQL → 加参数 → 改前端 → 测试 → 上线。频繁得很。而且大部分改动就是加一个WHERE条件技术含量为零纯粹是体力活。能不能让用户自己选字段、选条件、填值不用改代码两种入口列表页面的列信息从哪来两种来源两个入口入口一从Grid的列定义来——searchsetting(gridId, onComplete)列表页面已经有Grid组件Grid的LayoutManager管理了所有列的定义字段名、中文标题、数据类型。直接从这里提取functionsearchsetting(id,oncompeate){varlayoutbrowise.byId(id).getManager(LayoutManager);vararrlayout.getCells();vararr2[];for(vari0;iarr.length;i){arr2[i]{name:arr[i].name,label:arr[i].label,dataType:arr[i].dataType};}varcolsnewbrowise.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_columns和user_col_comments或SQL Server的sys.columns和sys.extended_properties查列名、数据类型、中文注释返回给前端。两个入口同一个对话框UI只是列数据的来源不同。对话框UI弹出一个窗口里面是一个可编辑的Grid4列列名类型说明字段名下拉框从传入的列信息填充显示中文标题存字段名关系符下拉框等于、like包含、大于等于、小于等于、大于、小于值文本框用户自由输入数据类型只读选字段名后自动填入用户可以添加多行条件每行一个条件全部用AND连接。点确定后对话框把所有行数据返回给回调函数。选字段名时自动填数据类型——cname()函数在下拉框变化时触发从列信息DataStore中查找匹配的字段把dataType填到第四列。这个细节很关键——后端拼SQL时需要根据数据类型决定怎么处理值。关键设计morewheres约定回调函数收到对话框返回的条件行后创建一个**名字固定为morewheres**的DataStorefunctionComplete2(value){if(!value)return;vararr[];for(vari0;ivalue.length;i){arr[i]{name:value[i].name,// 字段名val:value[i].val,// 值nation:value[i].nation,// 运算符datatype:value[i].dataType// 数据类型};}varmorenewbrowise.ds.DataStore(morewheres,arr);// 名字必须是morewheressearch(more);}morewheres这个名字是前后端之间的约定契约。前端必须用这个名字后端按这个名字取。透明注入三层接力这是整个设计最巧妙的部分。自定义条件从用户输入到最终生效经过三层接力业务Controller全程无感知。第一层route.java——拦截并存储框架的统一入口Servletroute.java在把请求分发给业务Controller之前先检查请求体里有没有叫morewheres的DataStore// route.javaDataStoremoredc.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.javaDataStoremoreAppContextContainer.getAppContext().getMore();if(more!nullmore.getRowset().getPrimary().size()0){pagingSelect.append( select * from ();}pagingSelect.append(sql);// 原始SQLif(more!nullmore.getRowset().getPrimary().size()0){StringmorewheregetMorewhere(more);pagingSelect.append() as cte333);pagingSelect.append( where );pagingSelect.append(morewhere);}最终生成的SQL长这样-- 原始SQLselect 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(inti0;imore.getRowset().getPrimary().size();i){if(i0)wheres and ;Rowrowmore.getRowset().getrow(i);Stringnamerow.getItemStringValue(name);// 字段名Stringnationrow.getItemStringValue(nation);// 运算符Stringvalrow.getItemStringValue(val);// 值Stringdatatyperow.getItemStringValue(datatype);wheresname 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;}wheresval;}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 onclicksearchsetting(grid, Complete2)自定义/button// 2. 加回调函数functionComplete2(value){if(!value)return;vararr[];for(vari0;ivalue.length;i){arr[i]{name:value[i].name,val:value[i].val,nation:value[i].nation,datatype:value[i].dataType};}varmorenewbrowise.ds.DataStore(morewheres,arr);search(more);}后端不需要改任何东西。业务Controller照常写查询分页照常用RowBounds框架自动处理。诚实地说不足之处SQL注入风险getMorewhere()里值是直接拼接进SQL的没有参数化。政务内网环境下风险可控用户是内部人员但不符合安全规范。改进方向是改用绑定变量。Oracle方言没实现OracleDialect没有处理morewheres只做了分页。如果部署到Oracle环境自定义条件会静默失效。当时项目跑在SQL Server上就没做。只支持AND多条件之间只能AND连接不支持OR、括号分组。政务场景下大部分查询是AND条件叠加够用了。如果需要OR那就不叫自定义查询了那叫高级查询——复杂度上升一个数量级不值得。条件不保存用户设的条件用完就没了下次还得重新填。如果要保存需要一个用户偏好表当时没做——用户没提这个需求。决策原则横切关注点不应该出现在业务代码里。自定义查询条件是一个典型的横切关注点——每个列表页面都可能需要但每个页面的处理逻辑完全一样。如果让每个业务Controller自己处理morewheres13个页面就是13份重复代码。把拦截、存储、注入全部做在框架管道里route.java → AppContext → PaginationInterceptor业务代码完全无感知。加一个功能只需要前端加一个按钮和回调后端零改动。这个思路和SM4加解密做在DBUtil管道里、审计日志做在commit()里、慢SQL检测做在AOP里——是同一个设计哲学管道做的事业务不该知道。你的项目里自定义查询是怎么做的是每个页面自己写还是框架统一处理欢迎评论区聊聊。作者许彰午| 非科班野生程序员深耕政务信息化20年标签#Java #MyBatis #动态查询 #分页 #低代码 #政务信息化