Skip to content

MongoDB - 关系

在 MongoDB 中,关系表示不同文档之间是如何逻辑连接的。这些连接可以通过两种主要策略进行建模:嵌入式(非范式化)和引用式(范式化)。选择哪种策略取决于您的应用程序的数据访问模式,它可以建模一对一(1:1)、一对多(1:N)和多对多(N:N)关系。

让我们使用一个常见场景来探讨这些概念:一个用户拥有多个地址。这代表一种一对多(1:N)关系。

一个现代的用户文档可能看起来像这样:

// users 集合
{
"_id": new ObjectId(),
"name": "Alex Doe",
"email": "alex.doe@example.com",
"createdAt": new ISODate()
}

以及一个关联的地址文档:

// addresses 集合(用于引用模型)
{
"_id": new ObjectId(),
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
}

策略 1:使用嵌入式文档进行建模

Section titled “策略 1:使用嵌入式文档进行建模”

在嵌入式方法中,您将相关数据直接放置在父文档内部。对于我们的用户和地址示例,我们将在用户文档中嵌入一个地址子文档数组。

// 带有嵌入式地址的用户
db.users.insertOne({
"_id": new ObjectId(),
"name": "Alex Doe",
"email": "alex.doe@example.com",
"createdAt": new ISODate(),
"addresses": [
{
"type": "Home",
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
},
{
"type": "Work",
"street": "456 Business Ave",
"city": "Metropolis",
"state": "NY",
"zip": "54321"
}
]
});

这种方法将所有相关数据整合到单个文档中,为读取操作提供了显著的性能优势。整个用户档案,包括所有地址,可以通过单次查询获取:

// 一次性检索用户及其地址
db.users.findOne({ "email": "alex.doe@example.com" });

优点:读取更快(无需数据库联接)、数据局部性,以及整个文档的原子更新。 缺点:可能导致大型文档(BSON 文档最大 16MB),如果嵌入信息在其他地方也需要,可能导致数据重复,如果嵌入数组变得非常大,写入速度可能会变慢。

引用式(或范式化)方法将相关数据保存在单独的集合中。父文档存储对相关文档的引用——通常是 _id 值。

// 引用地址 ID 的用户文档
db.users.insertOne({
"_id": new ObjectId(),
"name": "Alex Doe",
"email": "alex.doe@example.com",
"createdAt": new ISODate(),
"address_ids": [
new ObjectId("615cba23...1"), // 第一个地址文档的 ID
new ObjectId("615cba23...2") // 第二个地址文档的 ID
]
});
// 单独集合中的地址文档
db.addresses.insertMany([
{ "_id": new ObjectId("615cba23...1"), "street": "123 Main St", ... },
{ "_id": new ObjectId("615cba23...2"), "street": "456 Business Ave", ... }
]);

要检索用户的地址,您将使用 $lookup 聚合阶段,它执行与其他集合的左外连接。这是处理引用的现代、高效方式。

// 使用 $lookup 连接用户和地址
db.users.aggregate([
{
$match: { "email": "alex.doe@example.com" } // 查找用户
},
{
$lookup: {
from: "addresses", // 要连接的集合
localField: "address_ids", // 输入文档(用户)中的字段
foreignField: "_id", // 'from' 集合文档中的字段
as: "address_info" // 要添加到输入文档的新数组字段
}
}
]).pretty();

优点:避免数据重复,使文档更小,并且当相关数据需要频繁更新或独立访问时,这种方式更好。它对于复杂的 N:N 关系更灵活。 缺点:需要额外的 $lookup 操作,这可能比读取单个嵌入式文档慢。检索完整的实体需要多次集合访问。

选择并非总是二元的。混合方法通常是最佳的。对于包含性的、具有明确父子动态且主要一起访问的关系(例如,博客文章的评论),请使用嵌入式。当相关数据量大、独立访问或作为多对多关系的一部分时(例如,产品和类别),请使用引用式。