应采用嵌套对象结构存储表单数据,预留extend字段收容临时字段,表单版本与元数据分离存储于独立集合,数组项应带唯一ID并用$set精准更新,以应对语义漂移。

字段结构用嵌套对象,别硬塞 flat 字段
直接把所有表单项平铺成 field_0、field_1 这类命名,是 MySQL 思维残留。MongoDB 文档天然支持嵌套,字段名就是语义本身。比如请假表单存成:
{ "_id": "xxx", "formId": "leave-2024", "data": { "days": 3, "reason": "家里有事", "startDate": "2026-07-22" } }
这样查 data.days 直观,加字段不用改 schema,也不会出现大量 null 占位。如果字段类型混杂(字符串、数字、日期),flat 结构反而要额外存类型标记,徒增复杂度。
扩展字段统一收口到 extend,不污染主结构
业务上线后常有临时字段需求:比如突然要记录“审批人手机”或“是否加急”。这类字段不确定是否长期存在,也不适合进 data 主体。推荐预留一个 extend 字段:
{ "data": { "days": 3 }, "extend": { "urgent": true, "approverPhone": "138xxxx1234" } }
好处是:
• 主业务逻辑始终只读 data,稳定性高
• extend 可以任意增删,不影响索引和查询性能
• 后期归档时,可按需把高频 extend 字段迁入 data,避免文档膨胀
表单版本与元数据分离存储,避免数据错乱
动态表单必然涉及修改:今天加个“附件上传”,明天删掉“紧急联系人”。如果新旧表单共用同一份文档,老数据里可能缺字段、新数据里又多字段——查出来要么报错,要么字段值为 undefined。
正确做法是把表单定义(字段名、类型、校验规则)存在独立集合,如 form_schema;每条业务文档只存 formId 和 formVersion:
-
formId标识表单模板(如"leave") -
formVersion标识该文档基于哪个版本的 schema(如"v1.2") - 读取时先查
form_schema获取当前版本字段定义,再解析data
这样即使表单改了十次,历史数据依然能按原 schema 正确渲染和校验。
数组字段慎用 $push,优先用 $set 更新整个子文档
表单含重复项(如“添加多个附件”、“填写多条行程”)时,容易直接用 $push 往数组里塞新对象。但问题在于:
- 数组长度无上限,文档可能超 16MB 限制
- 每次
$push都要重写整个数组,写放大严重 - 无法对数组内某一项做原子更新(比如只改第 3 个附件的状态)
更稳妥的方式是把数组项转成带唯一 id 的对象,用 $set 更新指定路径:
{ "attachments": [ { "id": "att-001", "name": "合同.pdf", "status": "uploaded" } ] }
更新第 1 项状态:{"$set": {"attachments.0.status": "processed"}} —— 精准、轻量、可控。
真正麻烦的不是结构设计,而是字段语义漂移:同一个 reason 字段,初期是文本,后来变成下拉选项,再后来要求关联字典表。这种变化必须靠 formVersion + 元数据驱动来兜底,光靠文档结构扛不住。


















