Skip to main content

窄表的全生命周期管理

窄表建好之后,表单还会持续演进:加字段、删字段、改字段、甚至删除整个表单。本文说明这些操作在窄表模式下后端会自动做什么、不会做什么、以及需要人工介入的部分

所有窄表的 schema 变更都由后端服务 FormEntityDataStandaloneTableNormalizedService.createOrUpdateTable(formEntity) 统一处理,内部用 Liquibase 动态生成变更集(changeset)。它是幂等的:每次保存表单都会对比"表单期望的列"与"库中实际的列",只做必要的增量变更;无变更时直接返回。

速查表:各操作的自动化程度

操作后端是否自动执行说明
建表(首次开启窄表)✅ 自动id + 系统列 + 每字段业务列,并置 standalone_table_created=1
加字段✅ 自动加列保存表单时批量 ADD COLUMN,并回填 normalized_storage_field
改字段类型❌ 不改列类型窄表列类型宽松(多为 NVARCHAR/TEXT),一般无需变更;如需严格类型需手动 ALTER
改字段 token⚠️ 列名不变靠已存的 normalized_storage_field 保持列名稳定,历史数据不动
删字段不自动删列(设计如此)后端只识别并打印待删列,不执行 DROP,需 DBA 手动处理
删表单不自动删表(设计如此)物理窄表残留在库中,需 DBA 手动 DROP TABLE

为什么删除类操作不自动执行:删列 / 删表是不可逆的破坏性操作,一旦误删将造成数据永久丢失。CETA 有意将这类操作交由人工控制(by design),后端只做"只增不减"的安全变更,删除必须由 DBA 评估确认后手动执行。


加字段(自动)

给已开启窄表的表单新增字段后,保存表单即触发 createOrUpdateTable

  1. 后端对比期望列与现有列,把缺失的列收集起来,用一次批量 ADD COLUMN 加到窄表;
  2. 为新字段回填 form_entity_field.normalized_storage_field(即新列名);
  3. label/value 类字段(SELECT / ACL / CASCADER / TREE_SELECT)和 DATE 字段会同时加 两列xxx + xxx_label)。

无需任何手动操作。 验证:

-- 确认新列已存在,且字段的 normalized_storage_field 已回填
SELECT token, type, normalized_storage_field
FROM form_entity_field
WHERE form_entity_id = <feid> AND normalized_storage_field IS NOT NULL;

注意:

  • 新列默认可空,已有历史行在新列上的值为 NULL(符合预期)。
  • 列名由字段 token 转换而来(camelCase / 中划线 → snake_case,统一小写)。若列名撞数据库保留字或平台硬编码列名,会自动加前缀 ceta_pre_(见附录)。
  • LOOKUP 字段无存储列,会被跳过。

改字段类型 / 改字段 token

  • 改字段类型:后端不会 ALTER 已有列的类型。窄表列类型设计得比较宽松(文本类基本是 NVARCHAR(200) / TEXT,数字/日期也用 NVARCHAR),能容纳大多数类型变化,通常无需处理。若业务上确实需要严格的列类型(如改成真正的 NUMERIC / TIMESTAMP 以便排序、聚合),需 DBA 手动 ALTER COLUMN,并自行处理已有数据的类型转换。
  • 改字段 token:列名以字段已回填的 normalized_storage_field 为准,不随 token 改变而重命名,历史数据不受影响。这是有意的——避免频繁改名导致列漂移和数据错位。

加表(= 首次开启窄表)

"加表"就是把一个表单开启窄表,由后端 createOrUpdateTable 自动建表。完整流程见开启分表(窄表)。这里不再赘述。


删字段(需 DBA 手动删列)

破坏性操作 · 必须先备份 · 由 DBA 手动执行

删列会永久删除该列的所有数据,不可恢复。以下 SQL 仅供 DBA 在充分评估后执行,执行前必须完整备份该表。禁止在未备份、未确认的情况下直接运行。

当前行为:当你在表单上删掉一个字段后再保存,后端 createOrUpdateTable 会:

  • 计算出"库中存在、但表单已不再需要"的列(columnsToDelete);
  • 只打印一条日志提示这些列需要删除,不执行任何 DROP
    find column: [xxx, xxx_label], need to be drop, please confirm and do it manually!

所以删字段后,窄表里会残留这些废弃列(有数据但不再被平台读写)。是否清理、何时清理,由你和 DBA 决定。

手动删列步骤(DBA)

第 1 步 — 找出需要删除的列

对比"字段当前期望的列"与"窄表实际的列",人工确认差集:

-- 1a. 查表单当前所有字段对应的列名(期望保留的业务列)
SELECT token, type, normalized_storage_field
FROM form_entity_field
WHERE form_entity_id = <feid>;

-- 1b. 查窄表实际的列(PostgreSQL)
SELECT column_name
FROM information_schema.columns
WHERE table_name = '<standalone_table_name>'
ORDER BY ordinal_position;

用 1b 的结果减去:系统列(id / form_entity_id / deleted / created_time / updated_time / user_id / updated_user_id / flow_instance_id / thread_id / node_id / object_version)和 1a 中仍存在字段对应的列(含 _label),剩下的就是废弃列。务必逐列人工核对,不要凭猜测。

第 2 步 — 完整备份(强制)

-- 整表备份(数据量大时用 pg_dump 只备份该表,或备份到冷库)
CREATE TABLE bak_<standalone_table_name>_<yyyymmdd> AS
SELECT * FROM <standalone_table_name>;

第 3 步 — 删列(确认无误后执行)

-- ⚠️ 逐列删除,label/value 字段记得两列都删(xxx 和 xxx_label)
ALTER TABLE <standalone_table_name> DROP COLUMN <废弃列名>;
-- ALTER TABLE <standalone_table_name> DROP COLUMN <废弃列名>_label; -- 若是 label/value 字段

第 4 步 — 验证:确认列已删除、平台读写正常、备份表妥善保留一个发布周期后再清理备份。


删表(删表单后需 DBA 手动删表)

破坏性操作 · 必须先备份 · 由 DBA 手动执行

DROP TABLE永久删除整张表及其所有数据,不可恢复。执行前必须完整备份,并确认没有任何流程/页面/引用仍在使用该表单。由 DBA 手动执行。

当前行为:删除一个开启了窄表的表单(FormEntity)时,后端不会自动删除对应的物理窄表——createOrUpdateTable 里没有任何 drop table 逻辑(设计如此,避免误删)。因此删表单后,独立窄表会残留在数据库中,需要人工清理。

手动删表步骤(DBA)

第 1 步 — 确认表名与归属

删表单前先记下它的窄表名(删表单后 form_entity 记录可能已不在):

SELECT id, token, standalone, standalone_table_name
FROM form_entity
WHERE id = <feid>; -- 或按 pbc_token + token 查

第 2 步 — 完整备份(强制)

CREATE TABLE bak_<standalone_table_name>_<yyyymmdd> AS
SELECT * FROM <standalone_table_name>;
-- 数据量大时优先用 pg_dump 单表备份

第 3 步 — 确认无引用后删表

确认没有其它表单通过 ACL/引用字段指向该表单、没有流程/页面还在读它,再执行:

DROP TABLE <standalone_table_name>;

表名务必与 standalone_table_name 完全一致,且不能是平台系统保留表(见附录)。删表前反复核对表名,DROP TABLE 无法回滚。


附录:列名与表名规则

这些规则由后端 FormEntityDataStandaloneTableNormalizedService 决定,DBA 手动操作时需了解:

列名生成toColumnName):

  • 字段 token 从 camelCase / 中划线转为 snake_case 并统一小写(applicantNameapplicant_name)。
  • 若结果撞数据库 SQL 保留字(如 dateuserselect 等)或平台硬编码系统列名,自动加前缀 ceta_pre_(如 ceta_pre_date)。
  • 一旦字段的 normalized_storage_field 已回填,后续列名以它为准,不再随 token 变化。

label/value 双列SELECT / ACL / CASCADER / TREE_SELECT / DATE 字段各建两列——xxx(value)和 xxx_label(label)。删列时两列要一起处理。

系统列(不可删,平台读写依赖):idform_entity_iddeletedcreated_timeupdated_timeuser_idupdated_user_idflow_instance_idthread_idnode_idobject_version

保留表名:窄表名不能使用平台系统表名(如 form_entityform_entity_dataform_entity_data_acl 等约 130 个)。开启窄表时若表名冲突,后端会直接报错。

变更追踪:窄表的 schema 变更由 Liquibase 记录在独立的 changelog 表 form_entity_data_standalone_normalized_changelog_table(锁表 ..._lock_table),与平台主 changelog 隔离。