MongoDB 关系

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

告诉我你的业务场景,我帮你设计:

  1. 模型(用户、订单、商品、评论?)
  2. 读写比例(读多?写多?)
  3. 数据量(10w?1000w?)

回复 3 个关键词,我立刻画出完整 Schema + 查询代码!

文章已创建 2371

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部