当前位置: 首页 > news >正文

政务低代码平台实战①:5张表描述任意SQL——ea01-ea05元数据引擎设计

政务低代码平台实战①:5张表描述任意SQL——ea01-ea05元数据引擎设计

文章目录

  • 政务低代码平台实战①:5张表描述任意SQL——ea01-ea05元数据引擎设计
    • 背景
    • 5张表的结构
      • ea01:SQL语句头
      • ea02:FROM表
      • ea03:列/字段
      • ea04:JOIN条件
      • ea05:WHERE条件
    • commonSql:从元数据拼装SQL的核心类
      • SELECT拼装详解
      • WHERE条件的智能处理
      • Oracle vs SQL Server方言
    • system_cache:元数据缓存
    • 一个完整示例
    • 为什么不用MyBatis的动态SQL
    • 决策原则

非科班野生程序员,深耕政务信息化20年。政务系统有几百张业务表,每张表都要查询、新增、修改、删除——如果每条SQL都手写,维护成本爆炸。我的做法是用5张关系表描述一条完整的SQL,运行时动态拼装。90%的数据操作不用写一行SQL,剩下10%的复杂报表留了自定义SQL的口子。这篇拆解这个元数据引擎的设计。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。


背景

政务系统有两种数据访问:

  1. 标准CRUD— 单表或两三张表关联的增删改查,占90%
  2. 复杂报表— 五六张表关联、子查询、聚合,占10%

MyBatis的常规做法是每个SQL写一个mapper方法 + 一段XML。问题是:一个有50张表的政务系统,SELECT/INSERT/UPDATE/DELETE各一套,就是200段XML。字段改了?XML跟着改。表名改了?到处找。

我的做法:把SQL的结构拆成5张关系表存起来,运行时用一个类动态拼装。


5张表的结构

ea01:SQL语句头

一条SQL的身份证。ea01决定了这条SQL是SELECT还是INSERT还是UPDATE还是DELETE。

字段含义示例
eae001SQL唯一编号T_LEAVE_s
eae004SQL类型1=SELECT,2=UPDATE,3=DELETE,4=INSERT,5=存储过程
eae800是否自定义SQL1=自定义, 空=元数据拼装
eae801自定义SQL内容(当eae800=1时使用)select * from ...
eae994页面总列宽(用于表单渲染)6

eae800是个保险阀。90%的SQL走元数据拼装,但遇到五六张表关联的复杂报表,直接在eae801里写原生SQL。自定义SQL很少用,主要是给复杂报表留口子。

ea02:FROM表

SQL的FROM子句。一条SQL可以关联多张表。

字段含义示例
eae001SQL编号T_LEAVE_s
eae005表名T_LEAVE
eae006表别名a

拼出来就是from T_LEAVE a, T_DEPT b

ea03:列/字段

SELECT的字段列表,或INSERT/UPDATE的字段列表。

字段含义示例
eae001SQL编号T_LEAVE_s
eae006表别名a
eae007列名LEAVE_ID
eae008列别名(AS后面的名字)leave_id
eae009数据类型编码1=字符串,2=日期,3=数字,4=日期时间
eae991显示格式2=日期格式化
eae996显示宽度120(px)
eae997二级代码编码LEAVE_TYPE
eae998是否代码项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)

ea04:JOIN条件

表与表之间的关联条件。

字段含义示例
eae001SQL编号T_LEAVE_s
eae006左表别名.列名a.DEPT_ID
eae007
eae010右表别名b
eae011右表列名DEPT_ID

拼出来就是and a.DEPT_ID = b.DEPT_ID。用的是等值连接,放在WHERE里(不是JOIN ON)。政务系统的关联大多数是主外键等值连接,够用了。

ea05:WHERE条件

查询条件。这是最灵活的部分——支持常量和变量、等于和LIKE、括号。

字段含义示例
eae001SQL编号T_LEAVE_s
eae006表别名a
eae007列名PROC_INST_ID_
eae009数据类型编码1
eae012关系符01=等于,02=LIKE
eae013常量/变量标识1=常量, 空=变量
eae014常量值或变量名proc_inst_id_
eae015逻辑连接符and/or
eae016左括号1=加左括号
eae017右括号1=加右括号

eae013是关键区分:

  • eae013=1(常量):直接拼到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:

// 第一步:读缓存List<ea01Dao>resultea01=system_cache.get1(eae001+"-ea01");List<ea02Dao>resultea02=system_cache.get2(eae001+"-ea02");List<ea03Dao>resultea03=system_cache.get3(eae001+"-ea03");List<ea04Dao>resultea04=system_cache.get4(eae001+"-ea04");List<ea05Dao>resultea05=system_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(inti=0;i<resultea03.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 bwhere1=1anda.DEPT_ID=b.DEPT_IDanda.PROC_INST_ID_=?anda.AAE100='1'

WHERE条件的智能处理

WHERE条件不是全部拼上去,而是有条件地拼:

for(inti=0;i<resultea05.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: rownum"select * from (select row_1.*, rownum as rownum_ from ("+sql+") row_1) row_ where row_.rownum_ > ? and row_.rownum_ <= ?"// MSSQL: row_number() over"select * 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{privatestaticHashMap<String,List<ea01Dao>>cache1=newHashMap<>();// ea01privatestaticHashMap<String,List<ea02Dao>>cache2=newHashMap<>();// ea02privatestaticHashMap<String,List<ea03Dao>>cache3=newHashMap<>();// ea03privatestaticHashMap<String,List<ea04Dao>>cache4=newHashMap<>();// ea04privatestaticHashMap<String,List<ea05Dao>>cache5=newHashMap<>();// ea05publicstaticvoidinit(){// 启动时从数据库全量加载所有SQL元数据// key格式: "sqlId-ea01", "sqlId-ea02", ...}publicstaticvoidreset(){// 清空缓存并重新加载cache1.clear();cache2.clear();cache3.clear();cache4.clear();cache5.clear();init();}}

5个HashMap,key是sqlId-表名,value是DAO列表。commonSql每次拼SQL都直接读缓存,零数据库访问。

DDL引擎建完新表后会调system_cache.reset()刷新缓存,新表的元数据立即可用。


一个完整示例

假设要配置一张请假表的查询:

ea01(语句头):

eae001eae004eae800
T_LEAVE_s1 (SELECT)(空,走元数据)

ea02(FROM表):

eae005eae006
T_LEAVEa

ea03(列):

eae006eae007eae008eae009comments
aLEAVE_IDleave_id1请假编号
aLEAVE_DATEleave_date2请假日期
aLEAVE_TYPEleave_type1请假类型
aLEAVE_DAYSleave_days3请假天数

ea04(JOIN条件):无(单表查询)

ea05(WHERE条件):

eae006eae007eae012eae013eae014eae015
aPROC_INST_ID_01proc_inst_id_and
aAAE1000111and

前端传sqlId=T_LEAVE_s&proc_inst_id_=12345,后端自动拼出:

selecta.LEAVE_IDasleave_id,to_char(a.LEAVE_DATE,'yyyymmdd')asleave_date,a.LEAVE_TYPEasleave_type,a.LEAVE_DAYSasleave_daysfromT_LEAVE awhere1=1anda.PROC_INST_ID_=?anda.AAE100='1'

参数12345通过PreparedStatement绑定到第一个?


为什么不用MyBatis的动态SQL

MyBatis有<if><where><foreach>等动态SQL标签,能实现类似的条件拼装。区别在于:

MyBatis动态SQLea01-ea05元数据
SQL存在哪XML文件数据库表
谁维护开发人员开发人员或管理界面
改了要重启不用(MyBatis可以热加载)不用(清缓存即可)
新增查询要写代码不要(插几行数据)
方言切换要写两套XML自动切换
前端表单联动需要额外配置ea03自带控件类型和宽度

核心差异是元数据在前端也能用——ea03的字段注释、宽度、代码项标识,查询页面和表单页面都要用。如果用XML存SQL,这些信息得在另一个地方再存一份。ea01-ea05一份数据,SQL拼装和前端渲染都用。


决策原则

把SQL的结构从代码移到数据。

SQL的字段会变(加个字段)、条件会变(换个查询条件)、关联会变(多关联一张表)。这些东西不应该散落在几十个XML文件里。用关系表结构化地描述它们,一个类统一拼装,新增一条查询就是插几行数据的事。

自定义SQL(eae800=1)是保险阀——大部分场景走元数据,复杂报表直接写原生SQL,两条路都能到。但实际用得很少,90%都是元数据拼装。


如果你的系统也有大量重复的CRUD操作,可以考虑用元数据描述SQL。欢迎评论区聊聊你的做法。


系列导航:

  • 总纲:[政务低代码平台实战——从元数据引擎到可视化设计器的五个关键决策]
  • 上一篇:(总纲)
  • 下一篇:[政务低代码平台实战②:运行时DDL引擎,前端拖完字段后端直接建]

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

标签:#Java #低代码 #元数据驱动 #动态SQL #Oracle #SQLServer #政务信息化

http://www.jsqmd.com/news/1367189/

相关文章:

  • DevOps Interview Guide中的多语言支持:国际化项目要求
  • 关于图论【最短路径之Bellman_ford 算法(队列优化)|卡码网94.城市间货物运输的思考】
  • 韩国进口食品批发商怎么选才靠谱内行人分享三大筛选秘诀 - 官方资讯
  • Topit:重新定义macOS窗口置顶的颠覆性解决方案
  • 宜都闭口合同0增项装修:告别低价引流与中途加价的实战指南 - GrowthUME
  • dynDNS兼容方案:docker-ddns如何适配Fritz!Box、D-Link等主流路由器
  • 金华美发造型培训学校口碑哪家好推荐知美人 - 港焙西点-知美人美学
  • MoreToggles.css进阶技巧:如何优雅地调整切换按钮大小与颜色
  • 嘉兴美发造型培训学校口碑哪家好推荐知美人 - 港焙西点-知美人美学
  • BiliBiliToolPro完整指南:5分钟搭建B站自动化任务系统
  • 公共营养师证书怎么考:2026年报考条件、考试科目与备考全攻略 - 中科资质认证报考中心
  • 人工智能技术丛书(清华大学出版社)介绍
  • 做了三年韩食店,我才发现这家靠谱的韩国进口食品批发商 - 官方资讯
  • biniou离线部署教程:无需联网也能使用的AI创作工具,保护你的隐私数据
  • 2026年贵州做城市生命线安全工程建设的公司有哪些?
  • 2026年长春铁附件哪里有?深博电力及行业优质企业盘点,电话13756943000 - 自由和远方
  • 计算机毕业设计之高校教学资源管理系统的设计与实现
  • 乌鲁木齐老房漏水怎么办 卷材屋面施工外墙丙烯酸防水科普 ( 2026、8月份最新 ) - 宅仕达
  • 金庸群侠传C++复刻版:如何用现代游戏引擎架构重制经典武侠RPG
  • 终极指南:掌握40+功能的Escape From Tarkov离线训练器完整教程
  • 2026年深度解析:广东塞拉尼斯PA46代理商如何以技术选型重构耐高温材料供应链 - 变量人生001
  • 5大技术突破:AnythingLLM如何重新定义企业级文档智能处理
  • 【单片机毕业设计推荐】基于 STM32 的车辆防盗与温度监测系统设计与实现 基于 STM32 的车载环境安防监测报警系统设计(010706)
  • ImpromptuInterface性能优化指南:缓存策略与EmitProxy的高效使用
  • 新手采购韩国零食怕被坑?这5家高口碑批发商真实测评,省心又靠谱 - 官方资讯
  • bash-lib在CI/CD中的应用:打造高效自动化部署流程
  • GNU Wget2 vs curl:命令行下载工具全方位对比与选择指南
  • 香港律师公证单身证明怎么办理?理清思路少跑腿,快速拿证指南! - 指上通
  • 车辆异地托运公司哪家可靠 - 甄选测评官
  • Linux系统下RTL8852BE Wi-Fi 6驱动深度解析与性能调优指南