Javascript is required
实现原理发布于 2026-07-28审校于 2026-08-086 分钟阅读

嵌套 JSON 展平为关系表:数组、主键与外键如何映射

嵌套 JSON 适合表达一棵对象树,关系数据库则需要表、行和键。真正的展平不是把所有路径拼成列名,而是决定哪些对象成为表、哪些数组成为子表,以及如何保留父子关系。本文用订单数据说明映射过程,不讨论 SQL 注入或自动推断列类型。

JSON to SQLRelational ModelNested JSONForeign Key

一、问题概述:对象树与关系表的形状不同

一个订单可以包含客户对象和多个商品明细。若把 items[0].skuitems[1].sku 变成固定列,行数变化就会迫使表结构变化。更稳定的模型是 customersordersorder_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,并用重组测试验证没有丢失关系。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具