嵌套 JSON 展平为关系表:数组、主键与外键如何映射
嵌套 JSON 适合表达一棵对象树,关系数据库则需要表、行和键。真正的展平不是把所有路径拼成列名,而是决定哪些对象成为表、哪些数组成为子表,以及如何保留父子关系。本文用订单数据说明映射过程,不讨论 SQL 注入或自动推断列类型。
一、问题概述:对象树与关系表的形状不同
一个订单可以包含客户对象和多个商品明细。若把 items[0].sku、items[1].sku 变成固定列,行数变化就会迫使表结构变化。更稳定的模型是 customers、orders 和 order_items 三张表,子表用 order_id 指向父表。
二、最小复现:数组应成为多行,而不是带索引的列
输入中的 items 有两项,关系模型应得到两行明细,并保留它们属于哪个订单。
const input = {
id: "ord-100",
customer: { id: "cus-7", name: "Ada" },
items: [{ sku: "book", quantity: 1 }, { sku: "pen", quantity: 2 }],
};
// customers: [{ id: "cus-7", name: "Ada" }]
// orders: [{ id: "ord-100", customer_id: "cus-7" }]
// order_items: [{ order_id: "ord-100", sku: "book", quantity: 1 }, ...]三、根因:拆表需要业务关系,不只是递归路径
嵌套对象可能是一对一资料,也可能是有独立生命周期的实体;数组通常表示一对多,但也可能是无序标签集合。路径名本身不能决定主键、唯一约束、删除策略或历史保留。建模时先确定实体身份和关系,再决定列名。
四、推荐方案:先定义表边界和键,再生成行
稳定的输入 ID 可直接作为主键;没有稳定 ID 时,应在导入过程中生成 ID,并记录来源,不能用数组索引冒充跨批次身份。父表先写入,子表引用父键;同一对象在一个批次内只生成一行,数组元素按业务需要保留顺序字段。
五、完整代码:把订单树映射成三组行
以下 TypeScript 只负责建模和生成行,不执行 SQL。它将客户、订单和明细分开,并在发现必需字段缺失时抛出错误;真正写库时仍需使用参数化查询。
type OrderInput = {
id: string;
customer: { id: string; name: string };
items: Array<{ sku: string; quantity: number }>;
};
type Tables = {
customers: Array<{ id: string; name: string }>;
orders: Array<{ id: string; customer_id: string }>;
order_items: Array<{ order_id: string; position: number; sku: string; quantity: number }>;
};
function flattenOrder(order: OrderInput): Tables {
if (!order.id || !order.customer?.id) throw new Error("order and customer IDs are required");
if (!Array.isArray(order.items)) throw new TypeError("items must be an array");
return {
customers: [{ id: order.customer.id, name: order.customer.name }],
orders: [{ id: order.id, customer_id: order.customer.id }],
order_items: order.items.map((item, position) => {
if (!item.sku || !Number.isInteger(item.quantity) || item.quantity < 0) throw new TypeError("invalid item");
return { order_id: order.id, position, sku: item.sku, quantity: item.quantity };
}),
};
}
console.log(flattenOrder({ id: "ord-100", customer: { id: "cus-7", name: "Ada" }, items: [{ sku: "book", quantity: 1 }] }));六、常见错误方案
把所有嵌套路径拼成一张宽表会让可变数组变成列爆炸。只保存子表而不保存父键会失去归属,后续只能依赖位置猜测。把数组索引当永久 ID 会在排序、删除或重新导入后指向错误元素。把一个对象复制到每条明细也会造成更新异常和重复数据。
七、边界条件:空数组、null 与重复实体
空 items 应产生零条子行,而不是一条带 NULL 的明细;null 客户和缺失客户字段要分别定义导入策略。跨多个订单批次时,应按业务 ID 去重客户,而不是每次插入副本。若数组顺序有意义,保留 position;若无意义,则在模型中明确这一点。
八、如何验证映射结果
用一个客户、一个订单和两条明细测试行数与外键;再测试空数组、重复客户 ID、缺失订单 ID、负数量和 null。验证每条 order_items.order_id 都能在 orders 找到父行,并检查重新组合后的 JSON 是否符合允许的字段顺序和空值规则。
九、FAQ
问:所有嵌套对象都要拆表吗?答:不一定;稳定且不会独立查询的小对象可以保留为 JSON 列或展开为列。
问:数组一定是一对多表吗?答:通常适合,但标签集合、固定长度值和有序事件需要不同约束。
问:没有 ID 怎么办?答:生成导入 ID 并保存来源映射,不能把数组下标当作跨版本身份。
十、总结
嵌套 JSON 展平的核心是关系建模:数组拆成多行,对象边界形成表,子行通过主外键归属父行。先确定身份、唯一性和空值规则,再生成行;写入数据库时另行使用参数化 SQL,并用重组测试验证没有丢失关系。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
- PostgreSQL Documentation — SQL Syntax
PostgreSQL Global Development Group
- RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format
RFC Editor
相关文章
JSON 转 SQL:PostgreSQL 参数化查询与字面量转义边界
以 PostgreSQL 为例区分参数化写库与离线 SQL 导出,解释单引号、NULL、布尔值和数值的处理,避免把字符串替换误当成通用注入防护。
错误排查JSON 转 SQL 类型推断:MySQL 与 PostgreSQL 的边界
分开说明 MySQL 与 PostgreSQL 对 JSON、数字、字符串、数组、对象和 NULL 的类型建议,解释为什么样本推断不能替代明确的数据库 schema。
实现原理Myers 算法用于 JSON 数组 Diff 的边界与实现
说明 Myers 最短编辑脚本适合比较有序 JSON 数组,演示 insert、delete、equal 操作和路径 patch 的生成,并解释对象匹配与重复元素的限制。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具