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