政务低代码平台实战①5张表描述任意SQL——ea01-ea05元数据引擎设计文章目录政务低代码平台实战①5张表描述任意SQL——ea01-ea05元数据引擎设计背景5张表的结构ea01SQL语句头ea02FROM表ea03列/字段ea04JOIN条件ea05WHERE条件commonSql从元数据拼装SQL的核心类SELECT拼装详解WHERE条件的智能处理Oracle vs SQL Server方言system_cache元数据缓存一个完整示例为什么不用MyBatis的动态SQL决策原则非科班野生程序员深耕政务信息化20年。政务系统有几百张业务表每张表都要查询、新增、修改、删除——如果每条SQL都手写维护成本爆炸。我的做法是用5张关系表描述一条完整的SQL运行时动态拼装。90%的数据操作不用写一行SQL剩下10%的复杂报表留了自定义SQL的口子。这篇拆解这个元数据引擎的设计。最后感谢豆包、智谱、OpenCode决策是我做的代码是我搓的文字是他们总结的。背景政务系统有两种数据访问标准CRUD— 单表或两三张表关联的增删改查占90%复杂报表— 五六张表关联、子查询、聚合占10%MyBatis的常规做法是每个SQL写一个mapper方法 一段XML。问题是一个有50张表的政务系统SELECT/INSERT/UPDATE/DELETE各一套就是200段XML。字段改了XML跟着改。表名改了到处找。我的做法把SQL的结构拆成5张关系表存起来运行时用一个类动态拼装。5张表的结构ea01SQL语句头一条SQL的身份证。ea01决定了这条SQL是SELECT还是INSERT还是UPDATE还是DELETE。字段含义示例eae001SQL唯一编号T_LEAVE_seae004SQL类型1SELECT,2UPDATE,3DELETE,4INSERT,5存储过程eae800是否自定义SQL1自定义, 空元数据拼装eae801自定义SQL内容当eae8001时使用select * from ...eae994页面总列宽用于表单渲染6eae800是个保险阀。90%的SQL走元数据拼装但遇到五六张表关联的复杂报表直接在eae801里写原生SQL。自定义SQL很少用主要是给复杂报表留口子。ea02FROM表SQL的FROM子句。一条SQL可以关联多张表。字段含义示例eae001SQL编号T_LEAVE_seae005表名T_LEAVEeae006表别名a拼出来就是from T_LEAVE a, T_DEPT b。ea03列/字段SELECT的字段列表或INSERT/UPDATE的字段列表。字段含义示例eae001SQL编号T_LEAVE_seae006表别名aeae007列名LEAVE_IDeae008列别名AS后面的名字leave_ideae009数据类型编码1字符串,2日期,3数字,4日期时间eae991显示格式2日期格式化eae996显示宽度120(px)eae997二级代码编码LEAVE_TYPEeae998是否代码项1是comments中文注释请假类型eae700是否显示0隐藏数据类型编码eae009是关键——它决定了SQL里怎么转换类型也决定了前端用什么控件1 → 字符串Oracle: varchar2, MSSQL: varchar → 前端 TextBox 2 → 日期 Oracle: date, MSSQL: date → 前端 DateTextBox (yyyy-MM-dd) 3 → 数字 Oracle: number(18,2), MSSQL: decimal→ 前端 TextBox 4 → 日期时间Oracle: date, MSSQL: datetime → 前端 DateTextBox (yyyy-MM-dd HH:mm:ss)ea04JOIN条件表与表之间的关联条件。字段含义示例eae001SQL编号T_LEAVE_seae006左表别名.列名a.DEPT_IDeae007——eae010右表别名beae011右表列名DEPT_ID拼出来就是and a.DEPT_ID b.DEPT_ID。用的是等值连接放在WHERE里不是JOIN ON。政务系统的关联大多数是主外键等值连接够用了。ea05WHERE条件查询条件。这是最灵活的部分——支持常量和变量、等于和LIKE、括号。字段含义示例eae001SQL编号T_LEAVE_seae006表别名aeae007列名PROC_INST_ID_eae009数据类型编码1eae012关系符01等于,02LIKEeae013常量/变量标识1常量, 空变量eae014常量值或变量名proc_inst_id_eae015逻辑连接符and/oreae016左括号1加左括号eae017右括号1加右括号eae013是关键区分eae0131常量直接拼到SQL里如and a.AAE100 1有效标志eae013为空变量用?占位运行时从前端参数取值commonSql从元数据拼装SQL的核心类commonSql是整个引擎的心脏。2100多行代码核心就6个方法方法功能生成什么get()拼SELECTselect ... from ... where ...getCountSql()拼COUNTselect count(*) from ... where ...update()拼UPDATEupdate ... set ... where ...insert()拼INSERTinsert into ... values (...)delete()拼DELETEdelete from ... where ...selectSQL()拼装执行完整的查询流程SELECT拼装详解以get()方法为例展示元数据怎么变成SQL// 第一步读缓存Listea01Daoresultea01system_cache.get1(eae001-ea01);Listea02Daoresultea02system_cache.get2(eae001-ea02);Listea03Daoresultea03system_cache.get3(eae001-ea03);Listea04Daoresultea04system_cache.get4(eae001-ea04);Listea05Daoresultea05system_cache.get5(eae001-ea05);5次缓存读取拿到一条SQL的全部骨架。// 第二步判断是否自定义SQLif(1.equals(resultea01.get(0).getEae800())){// 直接走自定义SQL不从元数据拼returnselectSQLcustom(id,resultea01.get(0).getEae801(),map,dsName);}// 第三步拼SELECT子句sql.append(select \n);for(inti0;iresultea03.size();i){// 日期类型要加类型转换if(2.equals(resultea03.get(i).getEae009())){if(mssql.equals(dialect)){sql.append(CONVERT(varchar,);}else{sql.append(to_char();}}sql.append(resultea03.get(i).getEae006().resultea03.get(i).getEae007());if(2.equals(resultea03.get(i).getEae009())){if(mssql.equals(dialect)){sql.append(,112));// MSSQL日期格式}else{sql.append(,yyyymmdd));// Oracle日期格式}}sql.append( as resultea03.get(i).getEae008());}拼出来的SQL长这样selectto_char(a.LEAVE_DATE,yyyymmdd)asleave_date,a.LEAVE_TYPEasleave_type,a.LEAVE_DAYSasleave_daysfromT_LEAVE a,T_DEPT bwhere11anda.DEPT_IDb.DEPT_IDanda.PROC_INST_ID_?anda.AAE1001WHERE条件的智能处理WHERE条件不是全部拼上去而是有条件地拼for(inti0;iresultea05.size();i){if(1.equals(resultea05.get(i).getEae013())// 常量始终拼||(map.get(resultea05.get(i).getEae014())!null// 变量有值才拼!.equals(map.get(resultea05.get(i).getEae014())))){// 拼条件...}}这意味着如果前端没传某个查询参数对应的WHERE条件自动消失。不需要前端传查询所有的标志参数为空就不加条件。Oracle vs SQL Server方言两种数据库的差异集中在三个地方日期转换// Oracleto_char(a.LEAVE_DATE,yyyymmdd)to_date(?,yyyy-mm-dd)// MSSQLCONVERT(varchar,a.LEAVE_DATE,112)CONVERT(date,?)LIKE拼接// Oracle: 用 ||sql.append(||%);// MSSQL: 用 sql.append(%);分页// Oracle: rownumselect * from (select row_1.*, rownum as rownum_ from (sql) row_1) row_ where row_.rownum_ ? and row_.rownum_ ?// MSSQL: row_number() overselect * from(select cte1.*,row_number() over (order by orderBy desc) rownum_ from(sql) as cte1) as cte where rownum_ ? and rownum_ ?方言判断靠一个全局变量myDbProvider.getDialect()运行时根据配置决定。所有SQL拼装的地方都做了方言分支。system_cache元数据缓存元数据不每次查数据库启动时全加载到内存publicclasssystem_cache{privatestaticHashMapString,Listea01Daocache1newHashMap();// ea01privatestaticHashMapString,Listea02Daocache2newHashMap();// ea02privatestaticHashMapString,Listea03Daocache3newHashMap();// ea03privatestaticHashMapString,Listea04Daocache4newHashMap();// ea04privatestaticHashMapString,Listea05Daocache5newHashMap();// ea05publicstaticvoidinit(){// 启动时从数据库全量加载所有SQL元数据// key格式: sqlId-ea01, sqlId-ea02, ...}publicstaticvoidreset(){// 清空缓存并重新加载cache1.clear();cache2.clear();cache3.clear();cache4.clear();cache5.clear();init();}}5个HashMapkey是sqlId-表名value是DAO列表。commonSql每次拼SQL都直接读缓存零数据库访问。DDL引擎建完新表后会调system_cache.reset()刷新缓存新表的元数据立即可用。一个完整示例假设要配置一张请假表的查询ea01语句头eae001eae004eae800T_LEAVE_s1 (SELECT)空走元数据ea02FROM表eae005eae006T_LEAVEaea03列eae006eae007eae008eae009commentsaLEAVE_IDleave_id1请假编号aLEAVE_DATEleave_date2请假日期aLEAVE_TYPEleave_type1请假类型aLEAVE_DAYSleave_days3请假天数ea04JOIN条件无单表查询ea05WHERE条件eae006eae007eae012eae013eae014eae015aPROC_INST_ID_01proc_inst_id_andaAAE1000111and前端传sqlIdT_LEAVE_sproc_inst_id_12345后端自动拼出selecta.LEAVE_IDasleave_id,to_char(a.LEAVE_DATE,yyyymmdd)asleave_date,a.LEAVE_TYPEasleave_type,a.LEAVE_DAYSasleave_daysfromT_LEAVE awhere11anda.PROC_INST_ID_?anda.AAE1001参数12345通过PreparedStatement绑定到第一个?。为什么不用MyBatis的动态SQLMyBatis有if、where、foreach等动态SQL标签能实现类似的条件拼装。区别在于MyBatis动态SQLea01-ea05元数据SQL存在哪XML文件数据库表谁维护开发人员开发人员或管理界面改了要重启不用MyBatis可以热加载不用清缓存即可新增查询要写代码要不要插几行数据方言切换要写两套XML自动切换前端表单联动需要额外配置ea03自带控件类型和宽度核心差异是元数据在前端也能用——ea03的字段注释、宽度、代码项标识查询页面和表单页面都要用。如果用XML存SQL这些信息得在另一个地方再存一份。ea01-ea05一份数据SQL拼装和前端渲染都用。决策原则把SQL的结构从代码移到数据。SQL的字段会变加个字段、条件会变换个查询条件、关联会变多关联一张表。这些东西不应该散落在几十个XML文件里。用关系表结构化地描述它们一个类统一拼装新增一条查询就是插几行数据的事。自定义SQLeae8001是保险阀——大部分场景走元数据复杂报表直接写原生SQL两条路都能到。但实际用得很少90%都是元数据拼装。如果你的系统也有大量重复的CRUD操作可以考虑用元数据描述SQL。欢迎评论区聊聊你的做法。系列导航总纲[政务低代码平台实战——从元数据引擎到可视化设计器的五个关键决策]上一篇总纲下一篇[政务低代码平台实战②运行时DDL引擎前端拖完字段后端直接建]作者许彰午| 非科班野生程序员深耕政务信息化20年标签#Java #低代码 #元数据驱动 #动态SQL #Oracle #SQLServer #政务信息化