首页 / MongoDB 入门教程 / 一对多关系建模

MongoDB 入门教程

一对多关系建模

本教程共 50 篇 · 第 38 篇 · 更新于 2026-07-30 · 约 4 分钟阅读

MongoDBMongoDB 入门教程一对多数据建模反范式化内嵌数组

38. 一对多关系建模

本节目标:区分「有限一对多」和「无限一对多」,分别用内嵌数组和引用子表来建模。

一对多(One-to-Many)指一个实体对应多个子实体。一个用户有多个地址,一个用户有多笔订单。但这两类「多」差别很大,建模方式也完全不同。

38.1 有限增长:内嵌数组

用户的收货地址通常就那么几个,数量有限。这种「有限一对多」最适合内嵌成数组(Array)。

// 用户内嵌多个地址,一次读全
test> db.users.updateOne(
  { _id: 1 },
  { $set: {
      addresses: [
        { tag: "家", city: "杭州", detail: "文一西路 1 号" },
        { tag: "公司", city: "杭州", detail: "余杭塘路 2 号" }
      ]
  }}
)

查某用户所有地址,一条命令搞定,不用联表:

test> db.users.find({ _id: 1 }, { addresses: 1 })
Tip

数组天然表达一对多。只要子数据「数量有上限、总跟着父文档读」,就放心内嵌。

38.2 无限增长:改用引用

订单不一样。一个用户可能下成千上万笔订单,而且订单要经常单独查、单独分页。把它塞进用户文档,既撑大文档又违背访问模式。

正确做法:订单放 orders 集合,用 user_id 引用(Referencing)用户。

// 订单独立成集合,靠 user_id 指向用户
test> db.orders.insertOne({
  _id: 1001,
  user_id: 1,
  items: [{ product: "机械键盘", qty: 1, price: 399 }],
  total: 399,
  status: "paid",
  created_at: new Date()
})

查某用户的所有订单,按引用过滤即可:

test> db.orders.find({ user_id: 1 }).sort({ created_at: -1 }).limit(10)
Warning

引用一方「会无限增长」的数据时,别内嵌。文档 16MB 上限和读取效率都会出问题。

38.3 反范式化:在订单里快照商品

引用虽然省空间,但订单里的商品名、价格以后会变。为了保证「下单时多少钱就是多少钱」,我们把关键信息直接写进订单,这就是反范式化(Denormalization)。

看我们的 orders.items 结构:{ product, qty, price }。它把商品名和成交价固化了下来:

// 下单时把商品快照写进订单,与 products 集合解耦
test> db.orders.insertOne({
  user_id: 1,
  items: [{ product: "无线鼠标", qty: 2, price: 89 }],
  total: 178,
  status: "paid",
  created_at: new Date()
})

之后 products 里鼠标涨价到 99,历史订单依然显示 89,账目才对得上。

Note

反范式化是用「冗余」换「稳定与快读」。适合那些「写入后不应随原表变化」的字段,比如订单金额、收货地址快照。

38.4 取舍速查

特征内嵌数组引用子表
子数据量有限、有上限可能无限增长
访问方式总随父文档一起读常单独查询、分页
原子性父文档内原子更新需应用层或事务保证
例子用户地址、标签用户订单、评论

38.5 小结

一对多的关键判断是「增长是否有界」。有界就内嵌数组,无界就引用。需要对抗原表变化的值,用反范式化快照进文档。