65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MongoDB 的 $lookup 聚合器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2016 年 2 月 10 日

CPOL

9分钟阅读

viewsIcon

92919

深入探讨 $lookup 聚合器,并提供一对一、一对多、多对多和嵌套关系“查询”的示例

使用 MongoDB 的 $lookup 聚合器

我是一个 SQL 人——在我看来,关系型数据库是最好的。我从来没有迷恋过 NoSQL 数据库,因为我多年来处理的许多类型的数据在反规范化的文档数据库中表达效率低下。尽管如此,我偶尔也会关注 NoSQL 领域的发展,并惊讶地发现,从 2015 年 12 月发布的 MongoDB 3.2 开始,现在已经有了 $lookup 聚合器,它支持两个集合的左外连接(一对多)。

这对我很有吸引力,因为在我使用关系数据库创建语义数据库的工作中,NoSQL 的某些方面非常有吸引力,其中最重要的是无模式。然而,除了客户端过滤之外,无法关联两个 NoSQL 集合对我来说是一个障碍。好了,现在情况不同了。在我撰写一篇(即将发布)关于在 MongoDB 中实现语义数据库的文章时,我想专门写一些关于使用新的 $lookup 聚合器以及我与它一起工作时学到的东西。

$lookup 的历史

2015 年 9 月 30 日

阅读关于 $lookup 操作符的博客文章很有意思。正如 Eliot Horowitz 首次撰写的那样:

使用文档模型并不意味着连接(joins)没有用处。毫无疑问,当数据用于报告、商业智能和分析时,连接是有效的。

不过,你确实需要考虑他所写的内容,这些内容绝对偏向文档数据库。

关系数据库模型有几个理论上的优点,但也有许多实际的局限性,主要是需要在表之间连接数据。连接会影响性能,抑制扩展,并为除教科书示例以外的所有内容引入大量的技术和认知开销。

哇,真的吗?依我看,这恰恰说明了他对规范化数据库模式缺乏理解!

所以,2015 年 9 月 30 日

因此,它作为我们 MongoDB Enterprise Advanced 订阅的一部分提供。我们的理念始终是,文档是应用程序开发的正确模型。

这在社区中引起了相当大的反响(来自 9 月份 Eliot 文章的回复)

这太离谱了。将实际的引擎功能分配到企业版是令人困惑和恼火的。这很不幸,因为这意味着我此刻需要开始寻找其他文档数据库解决方案了,因为走这条路意味着将来会发生更多这样的事情。- Nef

令人作呕。我看不出将其移至企业版的理由,除了你们认为可以从中赚钱,因为它已经被人们长期要求并且由外部人士免费贡献了 - Nada

还有其他精彩的评论!

2015 年 10 月 20 日

正如 John A. De Goes 在 10 月份 撰写的那样:

虽然数据反规范化通常消除了在存储对象时进行繁琐的分裂和重组(这是关系世界中常见的做法)的需要,但即使在 MongoDB 中,连接也有有效的用例……MongoDB 选择创建一个仅限于企业版的管道运算符,违背了开源社区一些人的意愿,这将是一个有趣的决定,值得关注。

2015 年 10 月 29 日

Eliot 撰写道:

两周前,我宣布新的聚合管道阶段 $lookup(一个有限的左外连接运算符)将是 MongoDB Enterprise Advanced Server 独有的功能。我们的理由是,我们将 $lookup 视为其他企业功能的驱动因素,并且我们担心 $lookup 在社区版中的广泛可用性会导致用户将 MongoDB 视为关系数据库。

从技术上讲,我可以理解这种想法,但这也引出了我认为更为正确的观点:

尽管如此,有一点是清楚的:这让我们的用户感到不愉快的意外,这是我们永远不想做的。我们听到了你们的声音。$lookup 将成为社区功能。找到能使这一决定合理的原则(并且能够指导和解释未来的选择)对我们很重要,但不如我们社区的信心重要。

我们仍然担心 $lookup 可能被滥用,将 MongoDB 视为关系数据库。但与其限制其可用性,不如帮助开发人员了解何时使用它合适,何时它是反模式。

非常好。正如 De Goes 先生更新了他最初的博客文章:

以下帖子已过时。MongoDB 发布了 $lookup 操作符,包括 MongoDB Enterprise Advanced 和其社区版本。我们很高兴看到 MongoDB 重新考虑了这一决定。这表明了它对社区的关心以及对其反馈的重视,这对于建立和维护长期完全投入开源项目的信任非常有帮助。

尝试此代码需要什么

如果您从未在 C# 中使用过 MongoDB,您需要:

  • 下载并安装 MongoDB 服务器。
  • 下载并安装 2.2.0 或更高版本的 MongoDB .NET 驱动程序。
  • 运行 MongoDB 服务器,mongod.exe,可以在控制台窗口中运行,也可以作为服务运行。
    • mongod.exe 的 3.2 64 位版本通常位于 C:\Program Files\MongoDB\Server\3.2\bin
  • 可选地 下载并安装 RoboMongo,以便您可以使用漂亮的 GUI 来检查您的集合。

一对多连接

我不会在这里争论文档数据库与关系数据库的优缺点。两者各有优劣。本文的重点是如何使用 $lookup 聚合器,如果您确定需要在两个集合之间建立关系。

让我们看一个基本、过于简单的例子,其中一个集合包含一个代码(我们将以电话国家代码为例),而另一个集合是一个查找表,将代码与国家名称匹配。

首先,在 Mongo 控制台中,在 countryCode 集合中创建三个文档:

db.countryCode.insert([{code: 1}, {code: 20}, {code: 30}])

这将创建三个文档(所有屏幕截图均来自 RoboMongo):

接下来,创建一个查找表,将国家代码与国家名称配对:

db.countryCodeLookup.insert([{code: 1, name: "United States"}, 
                            {code: 20, name: "Egypt"}, {code: 30, name: "Greece"}])

这将产生以下文档:

现在我们可以使用 $lookup 操作符查询并连接这两个集合。在此查询中,我删除了 "_id" 字段以获得清晰度:

db.countryCode.aggregate([
{ $lookup: {from: "countryCodeLookup", localField: "code", 
 foreignField: "code", as: "countryName"} },
{ $project: {"code":1, "countryName.name":1, "_id":0} }
])

结果是两个文档被连接起来:

在上面的屏幕截图中,您会注意到每个 countryName 元素都包含一个文档数组。您可以通过添加具有相同代码的第二个文档来看到这一点:

db.countryCodeLookup.insert({code: 1, name: "Foobar"})

结果是(对于代码 1):

一对一连接

如果我们知道关系是 1:1,我们可以使用 $unwind 聚合器来解构数组,将返回的文档展平:

db.countryCode.aggregate([
{ $lookup: {from: "countryCodeLookup", localField: "code", 
           foreignField: "code", as: "countryName"} },
{ $unwind: "$countryName"},
{ $project: {"code":1, "countryName.name":1, "_id":0} }
])

这将消除内部数组:

但请注意,countryName 仍然在一个子文档中。我们可以使用投影来消除它:

db.countryCode.aggregate([
{ $lookup: {from: "countryCodeLookup", localField: "code", 
           foreignField: "code", as: "countryName"} },
{ $unwind: "$countryName"},
{ $project: {"code":1, "name": "$countryName.name", "_id":0} }
])

结果是

现在,我们有了一个格式精美的一对一文档集,有效地重现了 SQL 视图可以将一个表与第二个查找表连接起来所能做到的效果。

多对多连接

(注意:在这个例子中,我借鉴了我早期做过的一个例子,所以集合和字段名称的风格不符合标准!)

我们还可以使用 lookup 操作符来查询多对多关系。假设我有两个人,共享两个电话号码。我们按如下方式创建数据:

db.Person.insert({ID: 1, LastName: "Clifton", FirstName: "Marc"})
db.Person.insert({ID: 2, LastName: "Wagers", FirstName: "Kelli"})

db.Phone.insert({ID: 1, Number: "518-555-1212"})
db.Phone.insert({ID: 2, Number: "518-123-4567"})

请注意,我在这里使用的是我自己的 ID,这纯粹是说明性的,它使得编写测试示例更容易,但这种技术同样适用于对象 ID "_id" 字段。

接下来,我们将创建多对多关联文档:

db.PersonPhone.insert({ID: 1, PersonID: 1, PhoneID: 1})
db.PersonPhone.insert({ID: 2, PersonID: 2, PhoneID: 1})
db.PersonPhone.insert({ID: 3, PersonID: 2, PhoneID: 2})

这将产生一个包含 3 条记录的文档:

我们可以展开数组并为文档字段路径设置别名,以获得更简单的布局:

db.PersonPhone.aggregate([
{ $lookup: { from: "Person", localField: "PersonID", foreignField: "ID", as: "PersonName" } }, 
{ $lookup: { from: "Phone", localField: "PhoneID", foreignField: "ID", as: "PersonPhone" } }, 
{ $unwind: "$PersonName"},
{ $unwind: "$PersonPhone"},
{$project: {"LastName":"$PersonName.LastName", 
 "FirstName":"$PersonName.FirstName", "PhoneNumber":"$PersonPhone.Number", _id:0}} ])

嵌套查找

如果你有三个集合,其中集合 A 有一个指向集合 B 的“外键”,而集合 B 有一个指向集合 C 的“外键”怎么办?让我们回到我们的国家代码/国家名称集合,并添加一个带有单独国家代码的电话号码,这样我们就可以进行如下的连接:

电话号码 + 国家代码 + 国家名称

这有些牵强,但它将清楚地说明实现此目的的“技巧”。首先,创建几个电话记录:

db.phone.insert([{number: "555-1212", countryCode: 1}, {number: "851-1234", countryCode: 20}])

请注意,在进行下一次查找之前,我们必须展开每个生成的文档:

db.phone.aggregate([
{ $lookup: {from: "countryCode", 
 localField: "countryCode", foreignField: "code", as: "countryCode"} },
{ $unwind: "$countryCode"},
{ $lookup: {from: "countryCodeLookup", localField: 
 "countryCode.code", foreignField: "code", as: "country"} }
])

如果我们不这样做,我们的嵌套文档中将没有任何记录:

为什么会这样?

正如 Blakes Seven 在 Stack Overflow 上 写道

$lookup 聚合管道阶段不能直接与数组一起使用。设计的主要目的是作为“一对多”类型的连接(或者说是一个“查找”),用于可能的关联数据。“但其值被认为是单一的,而不是数组。因此,在执行 $lookup 操作之前,您必须先“反规范化”内容,才能使其正常工作。这意味着使用 $unwind。

我花了一点时间才找到这个解决方案!

同样,我们可以通过最终的 unwind 和别名字段路径投影进一步反规范化数据:

db.phone.aggregate([
{ $lookup: {from: "countryCode", localField: "countryCode", 
 foreignField: "code", as: "countryCode"} },
{ $unwind: "$countryCode"},
{ $lookup: {from: "countryCodeLookup", localField: "countryCode.code", 
  foreignField: "code", as: "country"} },
{ $unwind: "$country"},
{ $project: {"number": "$number", "countryCode": "$countryCode.code", 
 "country": "$country.name", "_id":0} }
])

结论

$lookup 聚合器在 Mongo NoSQL 范式中增加了创建规范化文档的能力。依我看,这是一个非常有用的功能,因为它将关联集合的问题推给了服务器,理论上(我也可以使用这个词,Eliot!)可以利用索引和其他性能改进。更不用说,为了在客户端关联两个或多个集合而必须下载的数据量大大减少了。

总的来说,我真的很享受我第一次真正探索 MongoDB 的过程。聚合器管道非常有趣,我很高兴 MongoDB 现在具备了关联两个或多个集合的能力。

限制

正如 $lookup 的文档所述:

“对同一数据库中一个非分片集合执行左外连接,以将文档从“已连接”的集合筛选出来进行处理。”

这意味着连接仅在以下情况下有效:

  1. 在同一台机器上的记录(非分片)
  2. 在同一数据库上的记录——您不能执行跨数据库的查找

如果这两项限制将来能被克服,那将很有意思。

支持连接的其他 NoSQL 数据库

根据 Wikipedia,以下 NoSQL 数据库支持连接(奇怪的是,在撰写本文时,MongoDB 不在此列表中,可能是因为它不支持 ACID(原子性、一致性、隔离性、持久性))。

  • ArangoDB
  • CouchDB
  • c-treeAce
  • HyperDex
  • MarkLogic
  • OrientDB

关于 MongoDB 和 ACID,根据这个回答

“使用 MongoDB 失去的是多集合(表)事务。MongoDB 中的原子修改器只能针对单个文档起作用。”

但是 ACID 和事务的整个问题是完全独立的话题!

历史

  • 2016年2月10日:初始版本
© . All rights reserved.