MongoDB 关系(Relationship)全解析
核心结论:MongoDB 是 文档型数据库,没有传统外键,但通过 嵌入(Embedding) 和 引用(Referencing) 两种方式灵活实现 一对一、一对多、多对多 关系。
一、两种关系模型对比
| 方式 | 说明 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 嵌入式(Embedded / Denormalized) | 子文档直接嵌套在父文档中 | 1. 单次查询获取完整数据 2. 原子性写操作 3. 高性能 | 1. 文档不能超过 16MB 2. 数据重复(更新需多处同步) | 一对一、一对少量多 |
| 引用式(Referenced / Normalized) | 文档通过 _id 引用其他集合 | 1. 无大小限制 2. 数据一致性强 3. 灵活更新 | 1. 需要多次查询(N+1 问题) 2. 无事务保证(除非用事务) | 一对多、多对多 |
二、关系类型实战
1. 一对一(1:1) → 推荐 嵌入
场景:用户 → 个人资料
// 嵌入式(推荐)
{
_id: ObjectId("..."),
name: "Alice",
email: "alice@x.com",
profile: {
avatar: "https://...",
bio: "Node.js 开发者",
location: "Beijing",
birthday: ISODate("1995-01-01")
}
}
优点:一次查询搞定,无 N+1
更新:直接$set整个profile
2. 一对多(1:N) → 分情况选择
场景:用户 → 订单(少量)
// 嵌入式(订单 ≤ 100 条)
{
_id: "user123",
name: "Bob",
orders: [
{ order_id: "o1", total: 99, status: "completed", date: ISODate() },
{ order_id: "o2", total: 150, status: "pending", date: ISODate() }
]
}
场景:用户 → 订单(大量)
// 引用式(推荐)
-- users 集合
{ _id: "user123", name: "Bob" }
-- orders 集合
{ _id: "o1", user_id: "user123", total: 99, status: "completed" }
{ _id: "o2", user_id: "user123", total: 150, status: "pending" }
查询用户所有订单:
db.orders.find({ user_id: "user123" })
3. 多对多(M:N) → 必须用 引用
场景:学生 ↔ 课程
-- students
{ _id: "s1", name: "Tom", course_ids: ["c1", "c2"] }
-- courses
{ _id: "c1", name: "数学", student_ids: ["s1", "s2"] }
{ _id: "c2", name: "英语", student_ids: ["s1"] }
双向引用,更新时需维护两边(可用事务)
三、高级技巧:聚合 $lookup 实现 JOIN
// 查询用户及其订单(引用式)
db.users.aggregate([
{ $match: { _id: "user123" } },
{
$lookup: {
from: "orders",
localField: "_id",
foreignField: "user_id",
as: "orders"
}
},
{
$project: {
name: 1,
"orders.order_id": 1,
"orders.total": 1,
"orders.status": 1
}
}
])
输出:
{
"_id": "user123",
"name": "Bob",
"orders": [
{ "order_id": "o1", "total": 99, "status": "completed" },
{ "order_id": "o2", "total": 150, "status": "pending" }
]
}
性能提示:为
user_id建索引
四、事务保证一致性(引用式必备)
const session = await client.startSession();
try {
session.startTransaction();
// 更新学生选课
await db.students.updateOne(
{ _id: "s1" },
{ $addToSet: { course_ids: "c3" } },
{ session }
);
// 更新课程学生列表
await db.courses.updateOne(
{ _id: "c3" },
{ $addToSet: { student_ids: "s1" } },
{ session }
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
五、设计决策表
| 关系类型 | 数据量 | 更新频率 | 查询模式 | 推荐方式 |
|---|---|---|---|---|
| 1:1 | 任意 | 任意 | 常一起读 | 嵌入 |
| 1:少量多 | ≤ 100 | 低 | 常一起读 | 嵌入 |
| 1:大量多 | > 1000 | 高 | 单独查询 | 引用 + $lookup |
| M:N | 任意 | 任意 | 灵活 | 双向引用 + 事务 |
六、Node.js + Mongoose 实现(推荐)
// User.js
const userSchema = new Schema({
name: String,
profile: {
avatar: String,
bio: String
},
orderIds: [{ type: Schema.Types.ObjectId, ref: 'Order' }] // 引用
});
// Order.js
const orderSchema = new Schema({
userId: { type: Schema.Types.ObjectId, ref: 'User' },
total: Number,
status: String
});
// 查询用户 + 订单(populate = $lookup)
const user = await User.findById(id).populate('orderIds');
七、反范式设计(Denormalization)技巧
// 在订单中冗余用户信息(提高读性能)
{
_id: "o1",
user_id: "u1",
user_name: "Alice", // 冗余
user_email: "a@x.com", // 冗余
total: 99
}
更新用户时:需同步更新所有订单(可用 MQ 或定时任务)
八、总结:黄金法则
| 法则 | 说明 |
|---|---|
| 读多写少 → 嵌入 | 减少查询次数 |
| 写多读少 → 引用 | 避免写放大 |
需要 JOIN → $lookup | 聚合管道 |
| 一致性要求高 → 事务 | 副本集 + MongoDB ≥ 4.0 |
| 文档 > 16MB → 必须引用 | 硬限制 |
实战案例:博客系统
// 嵌入评论(少量)
{
_id: "post1",
title: "MongoDB 关系",
content: "...",
comments: [
{ user: "u1", text: "赞!", date: ISODate() },
{ user: "u2", text: "学习了", date: ISODate() }
]
}
// 引用标签(多对多)
{
_id: "post2",
title: "Node.js 性能优化",
tag_ids: ["t1", "t2"]
}
你现在可以:
- 用 嵌入 存用户资料
- 用 引用 +
$lookup查用户订单 - 用 Mongoose
populate优雅实现 JOIN
告诉我你的业务场景,我帮你设计:
- 模型(用户、订单、商品、评论?)
- 读写比例(读多?写多?)
- 数据量(10w?1000w?)
回复 3 个关键词,我立刻画出完整 Schema + 查询代码!