Skip to content

MongoDB - 数据库引用

与传统 SQL 数据库不同,MongoDB 的灵活模式提供了多种建模数据关系的方式。选择嵌入(embedding)或引用(referencing)数据是模式设计中最关键的决策之一,它会影响应用程序的性能、数据完整性和可伸缩性。

嵌入式是指将相关数据存储在单个父文档中。此模式也称为非范式化(denormalization)。

考虑一个产品有一些评论。由于评论与产品紧密耦合,我们可以将它们作为数组嵌入到产品文档中。

{
_id: ObjectId("..."),
name: "Wireless Mouse",
price: 25,
// 评论直接嵌入在此处
reviews: [
{ username: "alice", rating: 5, comment: "Excellent product!" },
{ username: "bob", rating: 4, comment: "Good value for the price." }
]
}
  • 一对少关系: 当相关项的数量较少且有限时,此模式是理想选择。
  • 数据检索: 当您需要同时检索主文档及其相关数据时,嵌入式提供最佳性能,因为它只需要一次数据库读取。
  • 原子性: 对文档(包括其嵌入数据)的更新是原子性的。
  • 最佳应用场景: 用户的联系信息、购物车中的商品、产品的评论。

引用,或范式化(normalization),是指将相关数据存储在单独的集合中。您通过在一个文档中存储另一个文档的 _id 来创建它们之间的链接。这是大多数“一对多”关系推荐的方法。

一位作者可以撰写多篇文章。如果在每篇文章中都存储作者的完整详细信息将是冗余的。相反,我们在文章文档中存储作者的 _id。

// `authors` 集合
{
_id: ObjectId("author1"),
name: "Chris",
email: "chris@example.com"
}
// `posts` 集合
{
_id: ObjectId("post123"),
title: "Schema Design Best Practices",
content: "...",
// 手动引用作者
author_id: ObjectId("author1")
}
  • 一对多关系: 当“多”方数据量很大或无上限时(例如,事件的日志,热门文章的评论)。
  • 避免数据重复: 当引用数据量大且频繁更新时(例如,用户资料)。
  • 多对多关系: 例如,一个 students 集合和一个 courses 集合,通过存储两者引用的第三个集合进行链接。
  • 超出文档大小限制: 当嵌入会导致文档超出 16MB 限制时。

解析引用:使用 $lookup 的现代方法

Section titled “解析引用:使用 $lookup 的现代方法”

引用的主要缺点是它需要单独的查询来获取相关数据。解决此问题的现代高效方法是使用 $lookup 聚合阶段,它执行服务器端“连接(join)”操作。

// 此聚合管道查找一篇文章并“连接”其作者信息
db.posts.aggregate([
{
$match: { _id: ObjectId("post123") } // 查找特定文章
},
{
$lookup: {
from: "authors", // 要连接的集合
localField: "author_id", // 输入文档(posts)中的字段
foreignField: "_id", // 'from' 集合(authors)文档中的字段
as: "authorDetails" // 要添加到输入文档的新数组字段
}
}
])
// 结果包含文章,其作者详细信息嵌入在数组中:
/*
{
_id: ObjectId("post123"),
title: "Schema Design Best Practices",
author_id: ObjectId("author1"),
authorDetails: [
{ _id: ObjectId("author1"), name: "Chris", email: "chris@example.com" }
]
}
*/

DBRefs 是一种遗留的、正式的文档引用约定。一个 DBRef 是一个包含三个字段的文档:$ref(集合名称)、$id(被引用文档的 _id)和一个可选的 $db。

// 使用 DBRef 的文档示例
{
_id: ObjectId("user1"),
name: "Tom Benzamin",
// 指向 'addresses' 集合中某个文档的 DBRef
address: {
"$ref": "addresses",
"$id": ObjectId("addr1"),
"$db": "w3"
}
}

尽管它们存在,但 DBRefs 不再是推荐的关系建模方式。现代最佳实践强烈倾向于使用 $lookup 管道进行手动引用,因为:

  • 缺乏自动解析: 大多数 MongoDB 驱动程序不会自动解析 DBRefs。您仍然需要编写应用程序级代码来执行第二次查询,就像处理手动引用一样。
  • 增加复杂性: 与简单地存储 ObjectId 相比,它们会为您的文档增加不必要的冗余和复杂性。
  • $lookup 更优: $lookup 聚合阶段功能更强大、更灵活。它与简单的手动引用无缝协作,并能对连接的数据提供更多控制。

从嵌入式模式开始。 如果您的数据关系是一对少,并且数据通常一起访问,嵌入式模式会更快。如果嵌入式模式导致数据重复、文档过大,或者您有一对多关系,请切换到手动引用。在需要时,使用 $lookup 聚合管道高效检索相关数据。除非您有特定、充分的遗留原因或需要从单个字段引用多个集合的特殊用例,否则请避免使用 DBRefs。