Skip to content

10 · 元数据驱动的动态写入/读取(persist-meta 规范) ​

版本:1.8.0(起,1.8.0 同步演进) 定位:字段元数据(storageType)驱动的执行规范——复杂字段插入(对象/JSON/子表) 与流程回显读取成为通用能力,不再绑定任何框架的低代码设施。 与 DynamicTableWriter(spec 09)的关系:演进而非推翻——现有 SPI 保留;未提供元数据时的 缺省档仍是回落表结构探测(行为与 1.6.x 一致),另许可"未命中即失败"档,两档见 §3。

1. 目标与边界 ​

做(最小闭环) ​

  • 字段元数据(storageType)驱动的动态写入(NORMAL/EXPAND/JSON/ONE2ONE/ONE2MANY)
  • 按流程实例的动态读取回显(readByProcessInstance,与写入共用同一份元数据)
  • 元数据来源可插拔(SPI),内置 JSON 配置加载器(零依赖)

不做(明确边界) ​

通用条件分页 / 动态条件语法(m_EQ_xxx)/ 数据权限 / 排序——集成方查询体系(如 mldong tablePage) 的领域,双方共享字段元数据规范保持语义一致。

2. 元数据模型(八语言契约) ​

落地范围(2026-10 现读):Java/Go/Python/Node/MoonBit/C# 六栈按本节模型落地;PHP 元数据腿未落地 (IDynamicMetaProvider/MetaTableWriter 全仓零命中,读侧仅门面错误文案引用)、 Rust 为异构许可档(以元数据取代表结构探测;未命中即失败档见 §3)——发布形态见 §7。

2.1 JSON 配置字段语义(persist-meta/biz_leave.json,文件名=表名) ​

顶层:

字段类型缺省语义
tableNamestring必填业务表名(应与此 JSON 文件名一致)
primaryKeystringid主键列名(ONE2ONE/ONE2MANY 子表外键缺省回落它)
fieldsarray必填字段定义列表(见下)

fields[] 元素:

字段类型缺省语义
namestring必填表单字段名(实例 Variables 中 f_ 去前缀后的名字,如 f_title → title)
columnNamestringname 转下划线主表列名(如 companyName → company_name)
storageTypestring | numberNORMAL存储类型——名称或数字双解析(见下表)
expandFieldsobject无EXPAND 专用:子字段名 → 表列名映射({ "province": "province", "detail": "detail_addr" })
targetTablestring无ONE2ONE/ONE2MANY 专用:子表表名
foreignKeystring主表 primaryKey 列名ONE2ONE/ONE2MANY 专用:子表外键列

storageType 取值(对齐 mldong dev_schema_field 1-5,名称/数字双解析):

值名称语义
1NORMAL直写列(默认)
2EXPAND对象展开为多列(expandFields 定义子字段列映射)
3JSON对象/数组序列化为 JSON 串写列
4ONE2ONE子表单条递归插入(外键 = 主表主键,同事务)
5ONE2MANY子表多条递归插入(data 值为数组,同事务)

JSON 配置示例(persist-meta/biz_leave.json):

json
{
  "tableName": "biz_leave",
  "primaryKey": "id",
  "fields": [
    { "name": "companyName", "columnName": "company_name", "storageType": "NORMAL" },
    { "name": "address", "storageType": "EXPAND",
      "expandFields": { "province": "province", "city": "city", "detail": "detail_addr" } },
    { "name": "extra", "storageType": "JSON" },
    { "name": "items", "storageType": "ONE2MANY",
      "targetTable": "biz_leave_item", "foreignKey": "leave_id" }
  ]
}

3. SPI(集成方只实现一件事:提供元数据) ​

java
public interface IDynamicMetaProvider {
    /** 加载表元数据;未定义返回 null(缺省档:回落表结构探测,全 NORMAL 语义;另许可档见下) */
    TableMeta loadTableMeta(String tableName);
}
  • 内置 JsonMetaProvider:从文件系统/classpath 加载 persist-meta/*.json
  • 写、读共用:写入引擎与回显读取都按同一份元数据执行,storageType 语义两侧一致
  • 元数据未命中=两条许可档(audit8 R29/issues/195 定档;旧文本在此只写了回落档一条):
    • 回落探测档(缺省):provider 未命中(java 系形状=返回 null)⇒ 引擎回落物理表结构探测、 全列按 NORMAL 处理,行为与 1.6.x 一致。参考实现 java 即本档 (MetaTableWriter.java:18/:47 委托基础 writer + JdbcDynamicTableWriter.java:240/243 探测缓存)。
    • 未命中即失败档(许可):provider 未命中 ⇒ 该次 persist 动作以显式错误结束(rust 形状= get_table_meta 返回 None ⇒ Err("表元数据不存在: <表名>"))。选本档的实现, "表结构探测"这条腿归 provider 自身(rust 集成壳的 provider 直接探 information_schema, 表不存在时由它先报错,引擎那道未命中分支在真库路径上打不到)。
    • 两档不承诺跨语言统一(本 campaign 又一条明文分叉)。选失败档的两条硬义务: ① 错误可归因——文案必须带表名;② 不得"记 Ok 但什么都没写"——不落库必须是显式失败或留可诊断日志, 静默整单不落即红。用例判据=规范 08 场景 45;两档的落点由集成方在装配期自证。

4. 组件分工(读写职责分离) ​

写侧(DynamicTableWriter 接口不变)
   ├── JdbcDynamicTableWriter   现状:表结构探测,全 NORMAL
   └── MetaTableWriter          新:元数据驱动(NORMAL/JSON/EXPAND/子表递归)

读侧(流程回显最小闭环)
   JdbcTableReader              底层行查询(按列等值,limit)
   MetaTableReader              readByProcessInstance:storageType 反序列化 + 子表组装

5. 写入语义(MetaTableWriter) ​

storageType写入
NORMAL直写(列名匹配按 §5.1:禁分隔符归一、允许折大小写)
EXPAND对象字段展开为多列(address.province → province 列)
JSON对象/数组序列化为 JSON 串写列
ONE2ONE子表单条递归插入(外键 = 主表主键),同事务
ONE2MANY子表多条递归插入(data 值为数组),同事务

5.1 元数据腿的列匹配=大小写不敏感的精确列名匹配(audit8 R78,2026-10-16 owner 拍甲;口径同日现读修正) ​

本档的轴=要不要做「驼峰↔下划线分隔符归一」,不是要不要折大小写。 元数据腿(读侧 MetaTableReader、 写侧 MetaTableWriter、以及"未消费列"判定)按元数据给出的列名找列时:允许只折大小写,禁止做分隔符归一 (即不许把表单键 companyName 认成表列 company_name,也不许反过来)。

现读各栈形状(逐仓 cd 进去 grep,行号本轮复核过):

  • 同档=合规:java jeeflow-persist/.../meta/TableMeta.java:41-44(注释自述「大小写不敏感」)、 MetaTableReader.java:125、MetaTableWriter.java:186(三处都是 equalsIgnoreCase); go persist/meta.go:105+persist/meta_writer.go 取列腿;python jeeflow/meta.py:76(.lower()==.lower())+:344; node src/meta.ts:52/:311/:321(toLowerCase() 比)。
  • 同档合规(先前记成"更严"是转述失真,本轮读码更正):csharp MetaTableWriter.cs:154 用 Equals(columnName, StringComparison.OrdinalIgnoreCase)=折大小写、不归一分隔符,与 java 同档。 ⇒ 六栈(java/go/python/node/csharp+集成层)本就同档,违约面只剩 moon(+rust 待裁,见下)。
  • 违约档(本轮代码腿射程):moon persist/column_names_equal(strict~ 默认 false ⇒ 走 normalize_column() 归一), 且元数据腿五处调用(meta_reader.mbt:82/:95/:149、meta_writer.mbt:69/:71)全不传 strict~ ⇒ 恒宽松; rust jeeflow-persist/src/lib.rs:207-212 与 moon 同形(默认档 normalize_column), 其 strict_columns 由 with_strict_columns 显式开(:361 默认 false)⇒ 集成方不显式开就走归一档。

三条边界,免得被读宽或读窄:

  1. 只钉元数据腿。业务表落库那条轴(规范 09 §列匹配 的 1.6.4 默认宽松+strictColumnMatch 开关)不变, 本档不授权翻转那一档——两轴共用词但不同义,改元数据腿时不许顺手碰 writer.go::findColumn/normalizeColumn 那条腿。
  2. 列名对不上 ⇒ 显式失败或可诊断留痕(带表名+列名),不得"匹配到别的列"静默落值。
  3. 改法两选一、判据同形:moon 要么五处显式传 strict~,要么从元数据腿摘掉宽松支;rust 要么把默认档改判、 要么在元数据腿忽略 strict_columns。两种都要有负向用例咬住差异(同一份数据、列名只差分隔符 ⇒ 必判不匹配), 并各配一枚阳性对照(列名只差大小写 ⇒ 仍要取到值,证明没把 java 档整片打死)。

落地(2026-10-16 代码腿,owner 拍甲-1):

  • moon=五处显式传 strict=true(meta_reader.mbt:82/:95/:149、meta_writer.mbt:69/:71),默认宽松档原样留给 业务表那条轴;d9b2719。判据 persist/r78_meta_column_strict_test.mbt 七枚(writer 负向/阳性对照/逐字回归/ reader 两向配对/档级单点/幂等键直通闸——写侧的未消费字段判定改用同一把严格尺子,见下条)。 真库链 demo/cmd/t1_mysql 首跑曾照出一枚红(F149F 幂等键 process_instance_id 落 NULL)——那是 §9 自陈 "T1 两腿未做"的账。owner 裁甲′:幂等键由引擎按 java 同形注入 snake 键(moon 侧该腿既有= interceptor.mbt:426-427 if data.get("process_instance_id") is None 才注入,本轮一字未动; java 侧 PersistPostInterceptor.java:237 putIfAbsent),不改夹具期望值;本笔补的是写侧直通闸那把尺子 (驼峰键不再被误判成"已消费")⇒ 该真库腿转绿。
  • rust=采「元数据腿忽略 strict_columns」那一支,且现读更正一处本条的前提:rust 全联邦里 DynamicTableWriter 只有测试用的 InMemoryTableWriter(真库 writer 由集成侧自写),filter_editable 是本栈唯一的列匹配代码点 ⇒ 本栈没有独立的「业务表轴」一条腿,两轴同源。因此钉元数据腿必然撤掉本腿的 宽松档,with_strict_columns 随之作废为 no-op(#[deprecated] 保留签名不断档),而 1.6.4 那把宽松尺子 仍以公开件(normalize_column/column_names_equal/find_data_key)留给集成方 writer 自取—— 这恰是 java 的分工(元数据腿 MetaTableWriter 恒精确、基础 writer 才做归一)。 本改撤回了一条既有判据:issues/195 ④ 立的「驼峰表单键按宽松归一落到蛇形列」在 rust 的元数据腿不再成立, 撤回依据=owner 拍甲+上述现读结构,不是实现方自证(判据已按甲重切并配三刀变异,读数见 audit8-2026-10/notes/R78.md §12)。

复用现有能力:主键生成(含非自增雪花,生成器返回主键供子表外键)/ 系统字段 / 同链防重 / schema 限定探测。未消费字段(流程上下文 process_instance_id 等)直通。

中途更新(update,1.8.0):按元数据 storageType 组装 SET 列——NORMAL/JSON/EXPAND 参与更新;ONE2ONE/ONE2MANY 子表不参与中途更新(SYNC 任务推进只更新主表行状态, 子表数据变动走重新提交);未消费字段直通。无元数据回落基础 writer。

子表系统用户字段(1.8.0):子表递归插入时继承主表 apply_user_id (拦截器场景 = 流程 operator,putIfAbsent 子表单显式同名字段优先)——fillSystemFields 的用户列默认值可解析到 operator,避免 BIGINT create_user/update_user 列回落 "system" 严格模式报错(与主表行为一致)。

6. 读取语义(MetaTableReader) ​

java
// 按 relTableName + process_instance_id 回显一条(无分页/无条件)
Map<String, Object> readByProcessInstance(String tableName, Object processInstanceId);
storageType读取
NORMAL直读列
EXPAND反展开为对象(province/city/detail_addr → address.province/...)
JSON反序列化为对象/数组
ONE2ONE按外键=主表主键查子表单条,组装为对象
ONE2MANY按外键=主表主键查子表多条,组装为数组
  • 定位键 process_instance_id(写入幂等键同款)
  • 未消费列(流程上下文/系统字段)统一小写带出(跨方言一致)
  • EXPAND 展开列不重复平铺带出(1.8.0):province/city/detail_addr 已消费为 address 对象,不再作为顶层平铺键重复出现
  • 无元数据回落原始行(列名→值)

7. 版本与发布 ​

语言发布形态版本
Javajeeflow-persist 子模块(Maven Central)1.8.0
Gopersist/ 子包(go get 同 module)1.8.0
Pythonjeeflow.meta 模块(PyPI 随主包)1.8.0
Nodesrc/meta.ts(npm 随主包)1.8.0
MoonBitmldong/jeeflow-persist(persist 模块,mooncakes,独立 0.x 线)0.1.0
C#Mldong.Jeeflow.Persist(NuGet,独立 1.0.x 线)1.0.0
PHP元数据腿未落地(见 §2 注;persist 本体见 09)—
Rustjeeflow-persist(crates.io,独立 1.0.x 线;异构许可档见 §2 注/§3)1.0.0

jeeflow · 轻量级多语言工作流引擎