MongoDB-权威指南-全-
MongoDB 权威指南(全)
一、MongoDB 简介
Abstract
想象一下这样一个世界:使用数据库是如此简单,以至于你很快就会忘记自己正在使用它。想象一下这样一个世界:速度和可伸缩性刚刚好,不需要复杂的配置或设置。想象一下,你可以只专注于手头的任务,把事情做完,然后——只是为了改变一下——准时下班。这听起来可能有点异想天开,但是 MongoDB 承诺帮助您完成所有这些事情(甚至更多)。
想象一下这样一个世界:使用数据库是如此简单,以至于你很快就会忘记自己正在使用它。想象一下这样一个世界:速度和可伸缩性刚刚好,不需要复杂的配置或设置。想象一下,你可以只专注于手头的任务,把事情做完,然后——只是为了改变一下——准时下班。这听起来可能有点异想天开,但是 MongoDB 承诺帮助您完成所有这些事情(甚至更多)。
MongoDB(源自单词 humongous)是一种相对较新的数据库,它没有表、模式、SQL 或行的概念。它没有事务、ACID 遵从性、连接、外键或其他许多在凌晨时分容易让人头疼的特性。简而言之,MongoDB 是一个与您可能习惯的数据库非常不同的数据库,尤其是如果您过去使用过关系数据库管理系统(RDBMS)的话。事实上,你甚至可能会对缺乏所谓的“标准”功能感到惊讶。
不要害怕!在接下来的页面中,您将了解 MongoDB 的背景和指导原则,以及为什么 MongoDB 团队做出这样的设计决策。我们还将对 MongoDB 的特性列表进行一次短暂的浏览,提供足够的细节来确保您在本书的剩余部分完全迷上这个主题。
我们将从创建 MongoDB 背后的哲学和思想开始,以及一些有趣和有些争议的设计决策。我们将探索面向文档的数据库的概念,它们是如何组合在一起的,以及它们的优缺点。我们还将探索 JSON 并研究它如何应用于 MongoDB。最后,我们将逐步介绍 MongoDB 的一些显著特性。
回顾 MongoDB 理念
像所有项目一样,MongoDB 有一套帮助指导其开发的设计哲学。在这一节中,我们将回顾一些数据库的创建原则。
为正确的工作使用正确的工具
支撑 MongoDB 的最重要的理念是“一刀切”的概念。多年来,传统的关系(SQL)数据库(MongoDB 是一种面向文档的数据库)一直用于存储所有类型的内容。数据是否适合关系模型并不重要(关系模型在所有 RDBMS 数据库中使用,如 MySQL、PostgresSQL、SQLite、Oracle、MS SQL Server 等);反正数据是塞在里面的。部分原因是,一般来说,读取和写入数据库比写入文件系统更容易(也更安全)。如果你拿起任何一本教授 PHP 的书,比如 Jason Lengstorf (Apress,2009)的《PHP for Absolute Beginners 》,你可能会发现几乎立刻就会发现数据库是用来存储信息的,而不是文件系统。这样做要容易得多。虽然将数据库用作存储箱是可行的,但开发者总是不得不逆着流程工作。当我们没有按照预期的方式使用数据库时,这通常是显而易见的;任何曾经试图用稍微复杂的数据存储信息的人都知道我们在说什么,他们必须建立五个表,然后试图把它们放在一起。
MongoDB 团队决定不创建另一个试图为每个人做所有事情的数据库。相反,该团队希望创建一个处理文档而不是行的数据库,并且速度极快、可大规模伸缩且易于使用。为了做到这一点,团队不得不留下一些特性,这意味着 MongoDB 对于某些情况来说不是理想的候选。例如,它缺乏事务支持,这意味着您不想使用 MongoDB 来编写会计应用。也就是说,MongoDB 可能非常适合上述应用的一部分(比如存储复杂数据)。不过,这不是问题,因为没有理由不能将传统的 RDBMS 用于会计组件,将 MongoDB 用于文档存储。这样的混合解决方案相当普遍,你可以在《纽约时报》网站等生产应用中看到它们。
一旦你接受了 MongoDB 可能无法解决你所有问题的想法,你就会发现 MongoDB 非常适合解决某些问题,比如分析(想想你网站的实时 Google 分析)和复杂的数据结构(例如,博客帖子和评论)。如果您仍然不相信 MongoDB 是一个严肃的数据库工具,请随意跳到“查看特性列表”一节,在那里您会发现一个令人印象深刻的 MongoDB 特性列表。
Note
缺少事务和其他传统数据库特性并不意味着 MongoDB 不稳定,也不意味着它不能用于管理重要数据。
MongoDB 设计背后的另一个关键概念是,应该总是有多个数据库副本。如果一个数据库出现故障,那么可以简单地从其他服务器恢复。因为 MongoDB 的目标是尽可能快,所以它采取了一些捷径,这使得从崩溃中恢复变得更加困难。开发者认为,最严重的崩溃很可能会导致整台计算机停止工作;这意味着,即使数据库完全恢复,它仍然不可用。记住:MongoDB 并不试图成为每个人的一切。但是对于许多目的(比如构建 web 应用),MongoDB 可能是实现您的解决方案的一个非常棒的工具。
现在你知道 MongoDB 是从哪里来的了。它并不试图在每件事上都做到最好,它也很乐意承认它并不适合所有人。然而,对于那些选择使用它的人来说,MongoDB 提供了一个丰富的面向文档的数据库,它针对速度和可伸缩性进行了优化。它几乎可以在任何你想运行它的地方运行。MongoDB 的网站包括 Linux、Mac OS、Windows 和 Solaris 的下载。
MongoDB 在所有这些目标上都取得了成功,这就是为什么使用 MongoDB(至少对我们来说)有点像做梦一样。您不必担心将数据压缩到一个表中——只需将数据放在一起,然后传递给 MongoDB 进行处理。考虑这个真实世界的例子。Peter Membrey 最近开发的一个应用需要存储一组易贝搜索结果。可能有任意数量的结果(最多 100 个),他需要一种简单的方法将结果与数据库中的用户关联起来。
如果 Peter 一直使用 MySQL,他将不得不设计一个表来存储数据,编写代码来存储他的结果,然后编写更多的代码来将这些数据重新组合在一起。这是一个相当常见的场景,也是大多数开发者经常面对的一个场景。通常,我们只是继续做下去;然而,对于这个项目,他使用的是 MongoDB,所以事情有点不同。
具体来说,他添加了这行代码:
request[‘ebay_results’] = ebay_results_array
collection.save(request)
在这个例子中,request是彼得的文档,ebay_results是键,ebay_result_array包含来自易贝的结果。第二行保存更改。当他将来访问这个文档时,他将得到与以前完全相同格式的易贝结果。他不需要任何 SQL 他不需要执行任何转换;他也不需要创建任何新表或编写任何特殊代码——MongoDB 已经工作了。他很早就完成了工作,并且按时回家了。
缺乏对事务的内在支持
MongoDB 开发者的另一个重要设计决策是:数据库不包含事务语义(提供数据一致性和存储保证的元素)。基于 MongoDB 简单、快速和可伸缩的目标,这是一个可靠的权衡。一旦你把那些重量级的特性留在门口,横向扩展就变得容易多了。
通常,对于传统的 RDBMS,您可以通过购买更大、更强大的机器来提高性能。这是纵向扩展,但你只能做到这一步。通过水平扩展,您可以拥有许多功能较弱的小型机器,而不是一台大型机器。从历史上看,像这样的服务器集群非常适合负载平衡网站,但由于内部设计的限制,数据库一直是个问题。
你可能会认为这种支持的缺失构成了事务的破坏者;然而,许多人忘记了 MySQL 中最流行的一种表类型(MYISAM—也是默认的)也不支持事务。这个事实并没有阻止 MySQL 成为并保持十多年来占主导地位的开源数据库。与开发解决方案时的大多数选择一样,使用 MongoDB 将取决于个人偏好以及权衡是否适合您的项目。
Note
MongoDB 在与至少两台服务器协同使用时提供了持久性,这是推荐的生产部署的最低要求。在主服务器本身确认数据已被接受之前,可以让主服务器等待副本服务器确认收到数据。
虽然不能保证单服务器的持久性,但这种情况在未来可能会改变,目前是一个活跃的领域。
JSON 和 MongoDB
JSON (Java Script Object Notation)不仅仅是一种交换数据的好方法;这也是一种存储数据的好方法。RDBMS 是高度结构化的,有多个文件(表)存储各个部分。另一方面,MongoDB 将所有内容都存储在一个文档中。MongoDB 在这方面类似于 JSON,这种模型提供了一种丰富而富有表现力的数据存储方式。而且,JSON 有效地描述了给定文档中的所有内容,因此不需要事先指定文档的结构。JSON 实际上是无模式的(也就是说,它不需要模式),因为文档可以单独更新或独立于任何其他文档进行更改。另外,JSON 还通过将所有相关数据保存在一个地方提供了出色的性能。
MongoDB 实际上并不使用 JSON 来存储数据;相反,它使用由 MongoDB 团队开发的开放数据格式,称为 BSON(发音为 Bee-Son),是二进制 JSON 的缩写。在很大程度上,使用 BSON 而不是 JSON 不会改变您处理数据的方式。BSON 让 MongoDB 变得更快,让计算机更容易处理和搜索文档。BSON 还添加了一些标准 JSON 中没有的特性,包括添加类型来处理二进制数据的能力。我们将在本章后面的“使用面向文档的存储(BSON)”中更深入地研究 BSON。
JSON 的原始规范可以在 RFC 4627 中找到,它是由道格拉斯·克洛克福特编写的。JSON 允许复杂的数据结构以简单的、人类可读的文本格式表示,这种格式通常被认为比 XML 更容易阅读和理解。像 XML 一样,JSON 被设想为一种在 web 客户端(比如浏览器)和 web 应用之间交换数据的方式。当与它描述对象的丰富方式相结合时,它的简单性使它成为大多数开发者选择的交换格式。
您可能想知道这里的复杂数据结构是什么意思。历史上,数据是使用逗号分隔值(CSV)格式交换的(事实上,这种方法今天仍然非常普遍)。CSV 是一种简单的文本格式,用新行分隔行,用逗号分隔字段。例如,CSV 文件可能如下所示:
Membrey, Peter, +852 1234 5678
Thielen, Wouter, +81 1234 5678
人类可以查看这些信息,并很快看到正在传递什么信息。也可能不是——第三列中的号码是电话号码还是传真号码?它甚至可能是传呼机的号码。为了避免这种歧义,CSV 文件通常有一个头字段,其中第一行定义了文件中的内容。下面的代码片段将前面的示例向前推进了一步:
Lastname, Firstname, Phone Number
Membrey, Peter, +852 1234 5678
Thielen, Wouter, +81 1234 5678
好吧,这样好多了。但是现在假设 CSV 文件中的一些人有多个电话号码。您可以为办公室电话号码添加另一个字段,但是如果您想要多个办公室电话号码,您将面临一系列新的问题。如果您还想合并多个电子邮件地址,还会面临另一系列问题。大多数人都有不止一个地址,这些地址通常不能被整齐地定义为家庭或工作。突然间,CSV 开始显示出它的局限性。CSV 文件仅适用于存储平面的、没有重复值的数据。类似地,提供几个 CSV 文件并不少见,每个文件都有单独的信息。这些文件然后被组合(通常在 RDBMS 中)以创建整个画面。例如,一家大型零售公司可能会在每天结束时从其每个商店接收 CSV 文件形式的销售数据。这些文件必须组合在一起,公司才能看到它在某一天的表现。这个过程并不简单,而且随着所需文件数量的增加,它肯定会增加出错的几率。
XML 在很大程度上解决了这个问题,但是在大多数事情上使用 XML 有点像用大锤砸坚果:它可以工作,但是感觉有点矫枉过正。这是因为 XML 是高度可扩展的。XML 没有定义特定的数据格式,而是定义了如何定义数据格式。当您需要交换复杂且高度结构化的数据时,这可能很有用;然而,对于简单的数据交换来说,这通常会导致过多的工作。事实上,这种场景就是“XML 地狱”一词的来源
JSON 提供了一个快乐的媒介。与 CSV 不同,它可以存储结构化内容;但是与 XML 不同,JSON 使内容易于理解和使用。让我们重温一下前面的例子;但是,这次您将使用 JSON 而不是 CSV:
{
"firstname": "Peter",
"lastname": "Membrey",
"phone_numbers": [
"+852 1234 5678",
"+44 1234 565 555"
]
}
在这个版本的示例中,每个 JSON 对象(或文档)都包含理解它所需的所有信息。如果你看一下phone_numbers,你可以看到它包含了一个不同数字的列表。这个列表可以是你想要的那样大。您还可以更具体地说明正在记录的号码类型,如下例所示:
{
"firstname": "Peter",
"lastname": "Membrey",
"numbers": [
{
"phone": "+852 1234 5678"
},
{
"fax": "+44 1234 565 555"
}
]
}
这个版本的例子在某些方面做了更多的改进。现在你可以清楚地看到每个数字是什么。JSON 极具表现力,尽管手工编写 JSON 非常容易,但它通常是由软件自动生成的。例如,Python 包含一个名为(有点可预测)json的模块,它获取现有的 Python 对象,并自动将它们转换成 JSON。因为 JSON 在如此多的平台上得到支持和使用,所以它是交换数据的理想选择。
当您添加诸如电话号码列表之类的项目时,您实际上是在创建一个所谓的嵌入式文档。每当您添加复杂的内容,如列表(或数组,使用 JSON 中的术语)时,就会发生这种情况。一般来说,也有逻辑上的区分。例如,一个Person文档可能嵌入了几个Address文档。类似地,一个Invoice文档可能会嵌入许多LineItem文档。当然,嵌入的Address文档也可以有自己的嵌入文档,例如包含电话号码的文档。
当您决定如何存储信息时,就决定了是否选择嵌入特定的文档。这通常被称为模式设计。当 MongoDB 被认为是一个无模式的数据库时,提到模式设计可能显得有些奇怪。然而,虽然 MongoDB 不强迫您创建模式或强制您创建的模式,但是您仍然需要考虑如何将数据组合在一起。我们将在第 3 章中对此进行更深入的探讨。
采用非关系方法
提高关系数据库的性能通常很简单:购买更大、更快的服务器。这种方法非常有效,直到你没有更大的服务器可以购买。此时,唯一的选择是分散到两台服务器上。这听起来很容易,但是对于大多数数据库来说,这是一个绊脚石。例如,MySQL 和 PostgresSQL 都不能在两台服务器上运行单个数据库,这两台服务器都可以读写数据(通常称为主动/主动集群)。尽管 Oracle 可以通过其令人印象深刻的 Real Application Clusters (RAC)体系结构做到这一点,但如果您想使用该解决方案,您可能需要抵押贷款—实施基于 RAC 的解决方案需要多台服务器、共享存储和多个软件许可证。
您可能想知道为什么在两个数据库上拥有主动/主动集群如此困难。当你查询你的数据库时,数据库必须找到所有相关的数据并把它们连接在一起。RDBMS 解决方案有许多提高性能的巧妙方法,但它们都依赖于对可用数据的全面了解。这就是你碰壁的地方:当一半的数据在另一台服务器上时,这种方法根本不起作用。
当然,您可能有一个很小的数据库,它只是接收大量的请求,所以您只需要分担工作负载。不幸的是,在这里你碰到了另一堵墙。您需要确保写入第一台服务器的数据对第二台服务器可用。如果同时在两个独立的主服务器上进行更新,还会面临其他问题。例如,您需要确定哪个更新是正确的。您可能遇到的另一个问题是:有人可能会向第二个服务器查询刚刚写入第一个服务器的信息,但是该信息在第二个服务器上还没有更新。当您考虑所有这些问题时,就很容易理解为什么 Oracle 解决方案如此昂贵了—这些问题极难解决。
MongoDB 以一种非常聪明的方式解决了主动/主动集群问题——它完全避免了这些问题。回想一下,MongoDB 将数据存储在 BSON 文档中,因此数据是独立的。也就是说,尽管相似的文档存储在一起,但单个文档并不是由关系组成的。这意味着您需要的一切都在一个地方。因为 MongoDB 中的查询在文档中查找特定的键和值,所以这些信息可以很容易地传播到尽可能多的服务器上。每个服务器检查它拥有的内容并返回结果。这有效地实现了几乎线性的可伸缩性和性能。作为一个额外的奖励,它甚至不需要你拿出一个新的抵押贷款来支付这项功能。
诚然,MongoDB 不提供主/主复制,即两个独立的服务器都可以接受写请求。然而,它确实有分片,允许数据在多台机器上分割,每台机器负责更新数据集的不同部分。这种设计的好处是,虽然一些解决方案允许两个主数据库,但 MongoDB 可以像在两台机器上运行一样轻松地扩展到数百台机器。
选择性能还是功能
性能很重要,但是 MongoDB 也提供了大量的特性集。我们已经讨论了 MongoDB 没有实现的一些特性,您可能会对 MongoDB 部分通过明智地删除其他数据库共有的某些特性来实现其令人印象深刻的性能的说法有些怀疑。然而,有一些类似的数据库系统非常快,但也非常有限,比如那些实现键/值存储的系统。
memcached 就是一个很好的例子。这个应用是为提供高速数据缓存而编写的,它的速度快得令人麻木。当用于缓存网站内容时,它可以将应用的速度提高许多倍。这个应用被非常大的网站使用,例如脸书和 LiveJournal。
问题是这个应用有两个明显的缺点。首先,它是一个只读的数据库。如果停电,所有的数据都会丢失。第二,你实际上不能使用 memcached 搜索数据;您只能请求特定的密钥。
这些听起来像是严重的限制;但是,您必须记住 memcached 旨在解决的问题。首先,memcached 是一个数据缓存。也就是说,它不应该是一个永久的数据存储,而只是为现有的数据库提供一个缓存层。当您构建动态网页时,您通常会请求非常具体的数据(例如当前排名前十的文章)。这意味着您可以专门向 memcached 请求该数据——不需要执行搜索。如果缓存过期或为空,您应该像往常一样查询数据库,构建数据,然后将其存储在 memcached 中以备将来使用。
一旦您接受了这些限制,您就可以看到 memcached 如何通过实现非常有限的特性集来提供卓越的性能。顺便说一下,这种性能是传统数据库无法比拟的。也就是说,memcached 肯定不能取代 RDBMS。要记住的重要一点是,这是不应该的。
与 memcached 相比,MongoDB 本身功能丰富。为了有用,MongoDB 必须提供一组强大的特性,比如搜索特定文档的能力。它还必须能够将这些文档存储在磁盘上,这样它们就可以在重启后继续存在。幸运的是,MongoDB 提供了足够的特性,可以成为大多数 web 应用和许多其他类型应用的有力竞争者。
和 memcached 一样,MongoDB 也不是一个通用的数据库。与计算中的通常情况一样,必须做出权衡来实现应用的预期目标。
在任何地方运行数据库
MongoDB 是用 C++编写的,这使得在任何地方移植和/或运行应用都相对容易。目前,可以从 MongoDB 网站下载适用于 Linux、Mac OS、Windows 和 Solaris 的二进制文件。除了其他平台之外,Fedora 和 CentOS 也有各种官方版本。您甚至可以下载源代码并构建自己的 MongoDB,尽管建议您尽可能使用提供的二进制文件。所有二进制文件都有 32 位和 64 位版本。
Caution
32 位版本的 MongoDB 仅限于 2GB 或更小的数据库。这是因为 MongoDB 在内部使用内存映射文件来实现高性能。在 32 位系统上,任何大于 2GB 的内存都需要一些花哨的动作,这些动作不会很快,还会使应用的代码变得复杂。官方对此限制的立场是 64 位环境很容易获得;因此,增加代码复杂性不是一个好的权衡。实际上,64 位版本没有这样的限制。
MongoDB 的适度要求允许它运行在高性能的服务器或虚拟机上,甚至支持基于云的应用。通过保持简单并关注速度和效率,MongoDB 可以在您选择部署它的任何地方提供稳定的性能。
将所有东西组装在一起
在我们查看 MongoDB 的特性列表之前,我们需要回顾几个基本术语。MongoDB 不需要太多的专业知识就可以开始使用,许多 MongoDB 特有的术语可以粗略地翻译成您可能已经熟悉的 RDBMS 对等术语。不过,不要担心;我们将详细解释每个术语。即使您不熟悉标准的数据库术语,您仍然能够轻松理解。
生成或创建密钥
文档代表 MongoDB 中的存储单元。在 RDBMS 中,这将被称为行。然而,文档不仅仅是行,因为它们可以存储复杂的信息,比如列表、字典,甚至字典列表。与行是固定的传统数据库相比,MongoDB 中的文档可以由任意数量的键和值组成(您将在下一节中了解更多)。归根结底,一个键只不过是一个标签;它大致相当于您可能给 RDBMS 中的列起的名字。您使用一个键来引用文档中的数据。
在关系数据库中,应该总是有某种方法来唯一地标识给定的记录;否则就不可能引用特定的行。为此,应该包含一个保存唯一值的字段(称为主键)或一个可以唯一标识给定行的字段集合(称为复合主键)。
出于同样的原因,MongoDB 要求每个文档有一个惟一的标识符;在 MongoDB 中,这个标识符被称为_id。除非您为这个字段指定一个值,否则 MongoDB 将为您生成一个唯一的值。即使在成熟的 RDBMS 数据库中,对于是应该使用数据库提供的惟一键还是自己生成惟一键,也有不同的意见。最近,允许数据库为您创建密钥变得越来越流行。
其原因是人类创造的独特号码,如汽车注册号码,有一个令人讨厌的变化习惯。例如,2001 年,英国实施了与以前的系统完全不同的新的号牌方案。恰好 MongoDB 可以很好地应对这种类型的变化;但是,如果您使用牌照作为主键,您可能需要仔细考虑。当 ISBN(国际标准书号)方案从 10 位数字升级到 13 位数字时,可能会出现类似的情况。
以前,大多数使用 MongoDB 的开发者似乎更喜欢创建他们自己的惟一键,自己承担这一任务以确保数字保持惟一。但是今天,大多数人似乎都倾向于使用 MongoDB 为您创建的默认 ID 值。然而,就像使用 RDBMS 数据库一样,您选择的方法主要取决于个人偏好。我们更喜欢使用数据库提供的值,因为这意味着我们可以确保这个键是惟一的,并且独立于其他任何东西。如上所述,其他人更喜欢提供他们自己的密钥。
最终,你必须决定什么最适合你。如果您确信您的密钥是唯一的(并且可能保持不变),那么您应该可以随意使用它。如果您不确定您的密钥的唯一性或者您不想担心它,那么您可以简单地使用 MongoDB 提供的默认密钥。
使用键和值
文档由键和值组成。让我们再看一下本章前面讨论的例子:
{
"firstname": "Peter",
"lastname": "Membrey",
"phone_numbers": [
"+852 1234 5678",
"+44 1234 565 555"
]
}
键和值总是成对出现。与 RDBMS 不同,在 RDBMS 中,每个字段都必须有一个值,即使它是NULL(有点矛盾,这意味着未知),MongoDB 不要求文档有一个特定的值。例如,如果你不知道名单上某个人的电话号码,你就干脆把它删掉。这类事情的一个流行比喻是名片。如果你有传真号码,你通常会把它放在你的名片上;然而,如果你没有,你不写:“传真号码:无。”相反,你可以简单地忽略这些信息。如果 MongoDB 文档中没有包含键/值对,则认为它不存在。
实现集合
集合有点类似于表,但是它们远没有那么严格。收藏很像一个贴有标签的盒子。你家里可能有一个标有“DVD”的盒子,你可以把你的 DVD 放进去。这很有道理,但是如果你想的话,没有什么可以阻止你把 CD 甚至磁带放进这个盒子里。在 RDBMS 中,表是严格定义的,只能将指定的项放入表中。在 MongoDB 中,集合就是:相似项目的集合。条目不必相似(MongoDB 天生灵活);然而,一旦我们开始研究索引和更高级的查询,您很快就会看到在集合中放置相似项目的好处。
虽然你可以在一个系列中混合不同的物品,但没必要这样做。如果这个收藏被称为media,那么所有的 DVD、CD 和磁带都会在那里。毕竟,这些项目都有共同点,如艺术家姓名、发行日期和内容。换句话说,某些文档是否应该存储在同一个集合中确实取决于您的应用。就性能而言,拥有多个集合并不比只有一个集合慢。记住:MongoDB 是为了让你的生活更简单,所以你应该做任何你觉得对的事情。
最后但并非最不重要的一点是,集合是按需有效创建的。具体来说,当您第一次尝试保存引用集合的文档时,会创建一个集合。这意味着您可以按需创建集合(并不是说您必须这样做)。因为 MongoDB 还允许您动态地创建索引和执行其他数据库级命令,所以您可以利用这种行为来构建一些非常动态的应用。
了解数据库
也许在 MongoDB 中,将数据库视为集合的集合是最简单的方式。像集合一样,数据库可以按需创建。这意味着为每个客户创建一个数据库很容易——您的应用代码甚至可以为您做到这一点。除了 MongoDB 之外,您还可以使用其他数据库来实现这一点;然而,用 MongoDB 以这种方式创建数据库是一个非常自然的过程。也就是说,仅仅因为您可以用这种方式创建数据库,并不意味着您必须这样做,甚至不意味着您应该这样做。尽管如此,如果你想行使这种权力,你就有这种权力。
查看功能列表
现在您已经了解了 MongoDB 是什么以及它提供了什么,是时候浏览一下它的特性列表了。你可以在数据库网站 www.mongodb.org/ 找到 MongoDB 特性的完整列表;请务必访问该网站,获取最新列表。本章中的特性列表涵盖了相当多的幕后内容,但是使用 MongoDB 本身并不需要熟悉列出的每个特性。换句话说,如果你在回顾这个列表的时候感觉到你的眼睛开始闭上了,请随意跳到这一部分的末尾!
使用面向文档的存储(BSON)
我们已经讨论了 MongoDB 的面向文档的设计。我们也简要介绍了 BSON。正如您所了解的,JSON 使得以真实形式存储和检索文档变得更加容易,有效地消除了对任何类型的映射器或特殊转换代码的需求。这个特性也使得 MongoDB 更容易扩展,这是锦上添花。
BSON 是一个开放的标准;你可以在 http://bsonspec.org/ 找到它的规格。当人们听说 BSON 是 JSON 的二进制形式时,他们希望它比基于文本的 JSON 占用更少的空间。然而,这不一定是事实;事实上,在很多情况下,BSON 版本比 JSON 版本占用更多的空间。
你可能想知道为什么你应该使用 BSON。毕竟,CouchDB(另一个强大的面向文档的数据库)使用纯 JSON,因此有理由怀疑是否值得在 BSON 和 JSON 之间来回转换文档。
首先,我们必须记住,MongoDB 旨在提高速度,而不是节省空间。这并不意味着 MongoDB 浪费空间(它没有);然而,存储文档的少量开销是完全可以接受的,如果这样可以更快地处理数据的话(事实就是如此)。简而言之,BSON 更容易遍历(也就是说,浏览)和索引非常快。虽然 BSON 比 JSON 需要稍微多一点的磁盘空间,但是这种额外的空间不太可能成为问题,因为磁盘很便宜,而且 MongoDB 可以跨机器伸缩。这种情况下的折衷是很合理的:您可以用一点额外的磁盘空间来换取更好的查询和索引性能。
使用 BSON 的第二个主要好处是,可以方便快捷地将 BSON 转换成编程语言的本地数据格式。如果数据存储在纯 JSON 中,就需要进行相对高级别的转换。大量编程语言(如 Python、Ruby、PHP、C、C++和 C#)都有 MongoDB 驱动程序,每种驱动程序的工作方式都略有不同。使用简单的二进制格式,可以为每种语言快速构建原生数据结构,而不需要首先处理 JSON。这使得代码更简单、更快,这两者都符合 MongoDB 的既定目标。
BSON 也为 JSON 提供了一些扩展。例如,它使您能够存储二进制数据并合并特定的数据类型。因此,虽然 BSON 可以存储任何 JSON 文档,但有效的 BSON 文档可能不是有效的 JSON。这没关系,因为每种语言都有自己的驱动程序,可以在 BSON 之间来回转换数据,而不需要使用 JSON 作为中间语言。
归根结底,BSON 不太可能成为您使用 MongoDB 的重要因素。像所有伟大的工具一样,MongoDB 将静静地坐在后台,做它需要做的事情。除了可能使用图形工具来查看数据之外,您通常会使用您的母语工作,并让驱动程序担心如何持久化 MongoDB。
支持动态查询
MongoDB 对动态查询的支持意味着您可以运行一个查询,而无需事先计划。这类似于能够对 RDBMS 运行 SQL 查询。你可能想知道为什么这被列为一个特性;当然,这是每个数据库都支持的东西,对吗?
其实不是,比如 CouchDB(一般认为是 MongoDB 最大的“竞争对手”)不支持动态查询。这是因为 CouchDB 提出了一种全新的(当然也是令人兴奋的)思考数据的方式。传统的 RDBMS 有静态数据和动态查询。这意味着数据的结构是预先固定的——必须定义表,并且每一行都必须适合该结构。因为数据库预先知道数据的结构,所以它可以做出某些假设和优化,从而实现快速的动态查询。
CouchDB 彻底颠覆了这一点。作为一个面向文档的数据库,CouchDB 是无模式的,所以数据是动态的。然而,这里的新思想是查询是静态的。也就是说,在使用它们之前,您需要预先定义它们。
这并不像听起来那么糟糕,因为许多查询可以很容易地预先定义。例如,一个让你搜索一本书的系统可能会让你通过 ISBN 来搜索。在 CouchDB 中,您将创建一个索引,为所有文档构建一个 ISBNs 列表。当你输入 ISBN 时,查询速度非常快,因为它实际上不需要搜索任何数据。每当有新数据添加到系统中时,CouchDB 都会自动更新其索引。
从技术上讲,您可以针对 CouchDB 运行查询,而无需生成索引;然而,在这种情况下,CouchDB 必须先创建索引,然后才能处理您的查询。如果你只有一百本书,这就不是问题;然而,如果您要归档几十万本书,这会导致性能下降,因为每个查询都会一次又一次地生成索引。出于这个原因,CouchDB 团队不建议在生产中使用动态查询——即没有预定义的查询。
CouchDB 还允许您将查询写成map和reduce函数。如果这听起来像是很大的努力,那么你在一个好公司;CouchDB 的学习曲线有些严峻。公平地说,一个有经验的程序员可能会很快掌握它;然而,对于大多数人来说,学习曲线可能太陡了,以至于他们不愿意使用这个工具。
幸运的是,对于我们这些凡人来说,MongoDB 更容易使用。我们将在整本书中更详细地介绍如何使用 MongoDB,但这里有一个简短的版本:在 MongoDB 中,您只需提供想要匹配的文档部分,MongoDB 会完成其余部分。然而,MongoDB 可以做得更多。比如你想用map或者reduce函数就不会发现 MongoDB 缺少。同时,您可以轻松地使用 MongoDB 您不必预先了解该工具的所有高级特性。
为您的文档编制索引
MongoDB 包括对文档索引的广泛支持,当您处理成千上万的文档时,这个特性真的很方便。如果没有索引,MongoDB 将不得不依次查看每个单独的文档,以确定它是否是您想要查看的内容。这就像向图书管理员要一本书,然后看着他在库里走来走去,看每一本书。有了索引系统(库倾向于使用杜威十进制系统),他可以找到你要找的书所在的区域,并很快确定它是否在那里。
与库的书不同,MongoDB 中的所有文档都在_id键上自动索引。该键被视为特殊情况,因为您不能删除它;索引确保每个值都是唯一的。这个键的好处之一是可以保证每个文档都是唯一可识别的,而 RDBMS 不能保证这一点。
当您创建自己的索引时,您可以决定是否希望它们强制唯一性。如果您决定创建一个惟一的索引,您可以告诉 MongoDB 删除所有重复的索引。这可能是也可能不是您想要的,所以在使用此选项之前您应该仔细考虑,因为您可能会意外删除一半的数据。默认情况下,如果您尝试在具有重复值的键上创建唯一索引,将会返回错误。
在许多情况下,您会希望创建一个允许重复的索引。例如,如果您的应用按姓氏进行搜索,那么在姓氏键上建立索引是有意义的。当然,您不能保证每个姓氏都是唯一的;并且在任何合理大小的数据库中,副本实际上是可以保证的。
然而,MongoDB 的索引能力并不止于此。MongoDB 还可以在嵌入式文档上创建索引。例如,如果您在 address 键中存储了大量地址,则可以根据邮政编码创建一个索引。这意味着您可以基于任何邮政编码轻松地撤回文档,而且速度非常快。
MongoDB 通过允许复合索引更进一步。在复合索引中,两个或多个键用于构建给定的索引。例如,您可以构建一个结合了lastname和firstname标签的索引。搜索全名会非常快,因为 MongoDB 可以快速分离出姓,然后同样快速地分离出名。
我们将在第 10 章中更深入地研究索引,但只要说 MongoDB 已经涵盖了索引就够了。
利用地理空间索引
值得一提的一种索引形式是地理空间索引。MongoDB 1.4 中引入了这种新的专门的索引技术。您可以使用此功能来索引基于位置的数据,使您能够回答查询,例如在给定坐标集的特定距离内有多少项目。
随着越来越多的 web 应用开始利用基于位置的数据,这一特性将在日常开发中扮演越来越重要的角色。不过,就目前而言,地理空间索引仍然是一个小众功能;然而,如果你发现你需要它,你会很高兴它就在那里。
分析查询
一个内置的分析工具可以让您看到 MongoDB 是如何确定返回哪些文档的。这很有用,因为在许多情况下,只需添加一个索引就可以轻松地改进查询。如果您有一个复杂的查询,并且您不确定它为什么运行得这么慢,那么查询分析器可以为您提供非常有价值的信息。同样,您将在第 10 章中了解更多关于 MongoDB Profiler 的信息。
就地更新信息
当数据库更新一行(或者在 MongoDB 的情况下,更新一个文档)时,它有两种选择。许多数据库选择多版本并发控制(MVCC)方法,这种方法允许多个用户查看不同版本的数据。这种方法很有用,因为它确保了在给定的事务中,数据不会被另一个程序中途更改。
这种方法的缺点是数据库需要跟踪数据的多个副本。例如,CouchDB 提供了非常强大的版本控制,但这是以写出全部数据为代价的。虽然这确保了数据以可靠的方式存储,但也增加了复杂性并降低了性能。
另一方面,MongoDB 就地更新信息。这意味着(与 CouchDB 相反)MongoDB 可以在任何地方更新数据。这通常意味着不需要分配额外的空间,索引可以保持不变。
这种方法的另一个好处是 MongoDB 执行延迟写入。读写内存的速度非常快,但是写到磁盘要慢上千倍。这意味着您希望尽可能地限制对磁盘的读写。这在 CouchDB 中是不可能的,因为该程序确保每个文档都被快速写入磁盘。虽然这种方法可以保证数据安全地写入磁盘,但它也会显著影响性能。
MongoDB 只有在必要时才会写入磁盘,通常是每秒钟一次左右。这意味着,如果一个值每秒钟被更新多次——如果您将一个值用作页面计数器或用于实时统计,这种情况并不少见——那么该值将只被写入一次,而不是 CouchDB 要求的数千次。
这种方法使得 MongoDB 速度更快,但是,同样,这也是有代价的。CouchDB 可能比较慢,但它确实保证了数据安全地存储在磁盘上。MongoDB 没有这样的保证,这就是为什么传统的 RDBMS 可能是管理关键数据(如账单或应收账款)的更好的解决方案。
存储二进制数据
GridFS 是 MongoDB 在数据库中存储二进制数据的解决方案。BSON 支持在一个文档中保存高达 4MB 的二进制数据,这可能足以满足您的需求。例如,如果您想要存储个人资料图片或声音剪辑,那么 4MB 的空间可能会超出您的需要。另一方面,如果您想要存储电影剪辑、高质量的音频剪辑,甚至是几百兆大小的文件,那么 MongoDB 也可以满足您的需求。
GridFS 的工作原理是将关于文件的信息(称为元数据)存储在files集合中。数据本身被分解成称为块的片段,存储在chunks集合中。这种方法使得存储数据既容易又可伸缩;它还使得范围操作(例如检索文件的特定部分)更易于使用。
一般来说,您会通过编程语言的 MongoDB 驱动程序来使用 GridFS,所以您不太可能在这么低的级别上动手。和 MongoDB 中的其他东西一样,GridFS 是为速度和可伸缩性而设计的。这意味着,如果您想处理大型数据文件,MongoDB 可以胜任这项任务。
复制数据
当我们谈到 MongoDB 背后的指导原则时,我们提到 RDBMS 数据库为数据存储提供了 MongoDB 中所没有的某些保证。这些保证由于一些原因没有实现。首先,这些特性会降低数据库的速度。其次,它们会大大增加程序的复杂性。第三,人们认为服务器上最常见的故障是硬件故障,这将使数据无法使用,即使数据被安全地保存在磁盘上。
当然,这并不意味着数据安全不重要。如果您不能指望在需要时能够访问数据,那么 MongoDB 就没有多大用处。最初,MongoDB 提供了一个具有主从复制特性的安全网,其中在任何给定时间只有一个数据库是活动的,这种方法在 RDBMS 领域也很常见。这个特性已经被副本集所取代,基本的主从复制已经被废弃,不应该再使用了。
副本集有一个主服务器(类似于主服务器),它处理来自客户端的所有写请求。因为在给定的集中只有一个主服务器,所以它可以保证所有的写操作都得到正确的处理。发生写入时,会记录在主服务器的“操作日志”中。
操作日志由辅助服务器(可以有多个)复制,并用于使它们自己与主服务器保持同步。如果主节点在任何给定时间出现故障,其中一个辅助节点将成为主节点,并接管处理客户端写入请求的责任。
实现分片
对于那些参与大规模部署的人来说,自动分片可能是 MongoDB 最重要和最常用的特性之一。
在自动分片场景中,MongoDB 会为您处理所有的数据拆分和重组。它确保数据到达正确的服务器,并以最有效的方式运行和组合查询。事实上,从开发者的角度来看,与拥有一百个分片的 MongoDB 数据库对话和与单个 MongoDB 服务器对话没有任何区别。此功能尚未投入生产;然而,当它实现时,它将推动 MongoDB 的可伸缩性。
与此同时,如果您刚刚起步或者正在构建您的第一个基于 MongoDB 的网站,那么您可能会发现一个 MongoDB 实例就足以满足您的需求。然而,如果你最终建立了下一个脸书或亚马逊,你会很高兴你的网站建立在一种可以无限扩展的技术上。分片是本书第 12 章的主题。
使用 Map 和 Reduce 函数
对许多人来说,听到 MapReduce 这个词会让他们不寒而栗。在另一个极端,许多 RDBMS 的拥护者嘲笑map和reduce函数的复杂性。这对一些人来说很可怕,因为这些函数需要一种完全不同的方式来寻找和排序数据,许多专业程序员很难理解支撑map和reduce函数的概念。也就是说,这些函数提供了一种极其强大的数据查询方式。事实上,CouchDB 只支持这种方法,这也是它具有如此高的学习曲线的一个原因。
MongoDB 不要求您使用map和reduce函数。事实上,MongoDB 依赖于一个简单的查询语法,更类似于 MySQL 中的语法。然而,MongoDB 确实为那些想要的人提供了这些功能。map和reduce函数是用 JavaScript 编写的,在服务器上运行。map功能的任务是找到所有符合特定标准的文档。这些结果随后被传递给处理数据的reduce函数。reduce函数通常不返回文档集合;相反,它返回一个包含派生信息的新文档。一般来说,如果您通常在 SQL 中使用 GROUP BY,那么map和reduce函数可能是 MongoDB 中的合适工具。
Note
您不应该认为 MongoDB 的map和reduce函数是 CouchDB 所采用方法的拙劣模仿。如果您愿意,可以使用 MongoDB 的map和reduce函数来代替 MongoDB 固有的查询支持。
全新的聚合框架
MapReduce 是一个非常强大的工具,但是它有一个主要的缺点;它不太容易使用。许多数据库系统用于报告,特别是 SQL 数据库使这变得非常容易。如果你想对结果进行分组或者找出最大值和平均值,那么表达这个想法并得到你想要的结果是非常简单的。不幸的是,在 MapReduce 中实现这一点并不那么简单,实际上您必须自己完成所有的连接。这通常意味着一个简单的任务是不必要的挑战。
为了应对这种情况,MongoDB Inc(以前的 10gen)添加了聚合框架。它是基于管道的,允许您获取查询的各个部分,并将它们串在一起,以获得您想要的结果。这保持了 MongoDB 面向文档设计的优点,同时还提供了高性能。
因此,如果您需要 MapReduce 的所有功能,您仍然可以随时调用它。如果你只是想做一些基本的统计和数字处理,你会爱上新的聚合框架。你将在第 4 章和第 6 章中了解更多关于聚合框架及其命令的信息。
获得帮助
MongoDB 有一个很棒的社区,核心开发者非常活跃,容易接近,他们通常会竭尽全力帮助社区的其他成员。MongoDB 易于使用,并附带了很棒的文档;然而,知道你不是一个人,并且在你需要的时候有帮助是很好的。
访问网站
寻找更新信息或帮助的第一个地方是 MongoDB 网站(www://mongodb.org)。这个网站定期更新,包含所有最新的 MongoDB 优点。在这个站点上,您可以找到驱动程序、教程、示例、常见问题以及更多内容。
与 MongoDB 开发者聊天
MongoDB 开发者在 Freenode 网络上的#MongoDB(www.freenode.net)挂在互联网中继聊天(IRC)上。MongoDB 的开发者在纽约,但他们经常在这个频道聊天到深夜。当然,开发者确实需要在某个时候睡觉(咖啡只能工作这么久!);幸运的是,也有许多来自世界各地的知识渊博的 MongoDB 用户准备提供帮助。许多访问#MongoDB频道的人不是专家;然而,这里的气氛非常友好,所以他们还是留了下来。请随时加入#MongoDB频道,与那里的人们聊天——你可能会发现一些很棒的提示和技巧。如果你真的卡住了,你可能会很快回到正轨。
剪切和粘贴 MongoDB 代码
Pastie ( http://pastie.org )严格来说不是 MongoDB 网站;然而,如果你在#MongoDB中漂浮任意长的时间,你都会遇到它。Pastie 网站基本上允许你剪切和粘贴(因此得名)一些输出或程序代码,然后放到网上供他人查看。在 IRC 中,粘贴多行文本可能会很混乱或者难以阅读。如果你需要发布一些文字(比如三行或更多),那么你应该访问 http://pastie.org ,粘贴你的内容,然后将链接粘贴到你的新页面。
在谷歌群组上寻找解决方案
MongoDB 还有一个 Google 组叫做mongodb-user ( http://groups.google.com/group/mongodb-user )。这个群是一个提问或寻找答案的好地方。您还可以通过电子邮件与小组互动。与非常短暂的 IRC 不同,Google group 是一个伟大的长期资源。如果你真的想加入 MongoDB 社区,加入这个团体是一个很好的开始。
利用 JIRA 跟踪系统
MongoDB 使用 JIRA 问题跟踪系统。您可以在 http://jira.mongodb.org/ 查看跟踪站点,并积极鼓励您向该站点报告您遇到的任何错误或问题。社区认为报告此类问题是一件真正的好事。当然,您也可以搜索以前的版本,甚至可以查看下一个版本的路线图和计划更新。
如果你以前没有在 JIRA 发过帖子,你可能想先去参观一下 IRC 房间。你会很快发现你是否发现了新的东西,如果是这样,你会看到如何去报道它。
摘要
本章简要介绍了 MongoDB 带来的好处。我们已经了解了 MongoDB 创建和开发背后的哲学和指导原则,以及 MongoDB 开发者在实现这些理念时所做的权衡。我们还研究了与 MongoDB 结合使用的一些关键术语,它们是如何结合在一起的,以及它们大致的 SQL 对等词。
接下来,我们研究了 MongoDB 提供的一些特性,包括如何以及在什么地方使用它们。最后,我们简要介绍了社区的概况,以及在需要帮助时可以去哪里寻求帮助。
二、安装 MongoDB
Abstract
在第 1 章中,您体验了 MongoDB 可以为您做什么。在本章中,您将学习如何安装和扩展 MongoDB 以做更多的事情,使您能够将它与您喜欢的编程语言结合使用。
在第 1 章中,您体验了 MongoDB 能为您做什么。在本章中,您将学习如何安装和扩展 MongoDB 以做更多的事情,使您能够将它与您喜欢的编程语言结合使用。
MongoDB 是一个跨平台的数据库,您可以从 MongoDB 网站( www.mongodb.org )找到一个重要的可用包列表进行下载。大量的可用版本可能会让你很难决定哪个版本最适合你。对您来说,正确的选择可能取决于您的服务器使用的操作系统、您的服务器中的处理器种类,以及您是喜欢稳定的版本还是喜欢深入了解仍在开发中但提供令人兴奋的新功能的版本。也许您希望安装数据库的一个稳定版本和一个前瞻性版本。也有可能你还不完全确定应该选择哪个版本。无论如何,请继续读下去!
选择您的版本
当您查看 MongoDB 网站上的下载部分时,您会看到可供下载的包的一个相当简单的概述。您需要注意的第一件事是您将要运行 MongoDB 软件的操作系统。目前,有预编译的包可用于 Windows、各种版本的 Linux 操作系统、Mac OS 和 Solaris。
Note
这里需要记住的一件重要事情是该产品的 32 位版本和 64 位版本之间的区别。32 位和 64 位版本的数据库目前具有相同的功能,只有一个例外:32 位版本的数据库被限制为每台服务器大约 2GB 的总数据集大小。但是,64 位版本没有这种限制,所以对于生产环境,它通常比 32 位版本更受欢迎。此外,这些版本之间的差异可能会发生变化。
您还需要注意 MongoDB 软件本身的版本:有生产版本、以前的版本和开发版本。生产版本表明这是可用的最新稳定版本。当一个更新的、普遍改进或增强的版本发布时,先前的最新稳定版本将作为先前版本提供。这意味着这个版本是稳定和可靠的,但它通常有较少的可用功能。最后,是开发版。这个版本通常被称为不稳定版本。这个版本仍在开发中,它将包括许多变化,包括重要的新功能。尽管它还没有完全开发和测试,MongoDB 的开发者已经让公众可以测试或试用它。
了解版本号
MongoDB 使用“开发版本的奇数版本”方法。换句话说,您可以通过查看版本号的第二部分(也称为发布号)来判断一个版本是开发版还是稳定版。如果第二个数字是偶数,那么它是一个稳定的版本。如果第二个数字是奇数,那么它是一个不稳定的,或发展中的版本。
让我们仔细看看版本号的三个部分 A、B 和 C 中包含的三个数字:
- a,第一个(或最左边的)数字:代表主要版本,只有在有完整版本升级时才会改变。
- b,第二个(或中间)数字:代表发布号,表示一个版本是开发版还是稳定版。如果数量为偶数,则版本稳定;如果数量为奇数,则版本不稳定,被视为开发版本。
- c,第三个(或最右边的)数字:代表修订号;这是用于 bug 和安全问题的。
例如,在撰写本文时,可以从 MongoDB 网站获得以下版本:
- 2.4.3(生产发布)
- 2.2.4(先前版本)
- 2.5.0(开发版本)
在您的系统上安装 MongoDB
到目前为止,您已经了解了 MongoDB 的可用版本,并且——希望——能够选择一个。现在您已经准备好仔细研究如何在您的特定系统上安装 MongoDB。目前服务器的两个主要操作系统是基于 Linux 和 Microsoft Windows 的,所以本章将从 Linux 开始向您介绍如何在这两个操作系统上安装 MongoDB。
在 Linux 下安装 MongoDB
目前,基于 Unix 的操作系统是托管服务非常流行的选择,包括 web 服务、邮件服务,当然还有数据库服务。在这一章中,我们将带你了解如何让 MongoDB 在一个流行的 Linux 发行版 Ubuntu 上运行。
根据您的需要,您有两种方式在 Ubuntu 下安装 MongoDB:您可以通过所谓的存储库自动安装软件包,也可以手动安装。接下来的两节将向您介绍这两个选项。
通过存储库安装 MongoDB
储存库基本上是装满软件的在线目录。每个软件包都包含关于版本号、先决条件和可能的不兼容性的信息。当您需要安装需要先安装另一个软件的软件包时,此信息非常有用,因为可以同时安装必备软件。
Ubuntu(和其他基于 Debian 的发行版)中的默认库包含 MongoDB,但它们可能是该软件的过时版本。因此,让我们告诉apt-get(您用来从存储库中安装软件的软件)查看一个自定义存储库。为此,您需要将下面一行添加到您的存储库列表中(/etc/apt/sources.list):
debhttp://downloads-distro.mongodb.org/repo/ubuntu-upstart
接下来,您需要导入 10gen 的公共 GPG 密钥,用于对包进行签名,确保它们的一致性;您可以通过使用apt-key命令来完成:
$ sudo apt-key adv --keyserverkeyserver.ubuntu.com
完成后,您需要告诉 apt-get 它包含新的存储库;您可以使用 apt-get 的update命令来实现:
$ sudo apt-get update
这一行让 aptitude 知道您手动添加的存储库。这意味着您现在可以告诉 apt-get 自己安装软件。要做到这一点,可以在 shell 中键入以下命令:
$ sudo apt-get install mongodb-10gen
这一行安装来自 MongoDB 的当前稳定(生产)版本。如果您希望从 MongoDB 安装任何其他版本,您需要指定版本号。例如,要从 MongoDB 安装当前不稳定的(开发)版本,请键入以下命令:
$ sudo apt-get install mongodb-10gen=2.5.0
这就是全部了。至此,MongoDB 已经安装完毕,并且(几乎)可以使用了!
Note
在运行旧版本 MongoDB 的系统上运行apt-get update会将软件升级到可用的最新稳定版本。您可以通过运行以下命令来防止这种情况发生:
echo "mongodb-10gen hold" | sudo dpkg --set-selections
手动安装 MongoDB
接下来,我们将介绍如何手动安装 MongoDB。考虑到用 aptitude 自动安装 MongoDB 是多么容易,您可能想知道为什么要手动安装该软件。首先,并不是所有的 Linux 发行版都使用 apt-get。当然,他们中的许多人会这样做(主要包括基于 Debian Linux 的那些),但有些人不会。此外,打包仍然是一项进行中的工作,因此可能会出现这样的情况,即有些版本还不能通过存储库获得。也有可能您想要使用的 MongoDB 版本没有包含在存储库中。手动安装软件还可以让您同时运行多个版本的 MongoDB。
你已经决定使用哪个版本的 MongoDB,你已经从他们的网站 http://mongodb.org/downloads 下载到你的主目录。接下来,您需要使用以下命令提取包:
$ tar xzf mongodb-linux-x86_64-latest.tgz
该命令将包的全部内容提取到一个名为mongodb-linux-x86_64-xxxx-yy-zz的新目录中;该目录位于您的当前目录下。这个目录将包含许多子目录和文件。包含可执行文件的目录称为bin目录。我们将很快介绍哪些应用执行哪些任务。
但是,您不需要做任何进一步的工作来安装应用。事实上,手动安装 MongoDB 并不会花费太多时间——根据您需要安装的其他内容,它甚至可能会更快。然而,手动安装 MongoDB 也有一些缺点。例如,默认情况下,您刚刚提取并在bin目录中找到的可执行文件不能从除了bin目录之外的任何地方执行。因此,如果您想要运行mongod服务,您将需要直接从前面提到的bin目录中这样做。这个缺点突出了通过存储库安装 MongoDB 的好处之一。
在 Windows 下安装 MongoDB
微软的 Windows 也是服务器软件的热门选择,包括基于互联网的服务。
Windows 没有像 apt-get 这样的存储库应用,所以您需要从 MongoDB 网站下载并解压缩该软件来运行它。是的,前面的信息是正确的。您不需要完成任何设置过程;安装软件是一件简单的事情,只需下载软件包、解压缩并运行应用本身。
例如,假设您已经决定为您的 64 位 Windows 2008 服务器下载 MongoDB 的最新稳定版本。首先将包(mongodb-win32–x86_64-x.y.x.zip)解压到您的C:\驱动器的根目录。此时,您需要做的就是打开一个命令提示符(开始➤运行➤ cmd ➤ OK)并浏览到您提取内容的目录:
> cd C:\mongodb-win32–x86_64-x.y.z\
> cd bin\
这样做会将您带到包含 MongoDB 可执行文件的目录。这就是全部内容:正如我前面提到的,不需要安装。
运行 MongoDB
终于,你准备好动手了。您已经了解了从哪里获得最适合您的需求和硬件的 MongoDB 版本,并且还了解了如何安装软件。现在终于到了研究运行和使用 MongoDB 的时候了。
先决条件
在启动 MongoDB 服务之前,您需要为 MongoDB 创建一个数据目录来存储它的文件。默认情况下,MongoDB 将数据存储在基于 Unix 的系统(比如 Linux 和 OS X)的/data/db目录中,以及 Windows 的C:\data\db目录中。
Note
MongoDB 不会为你创建这些数据目录,所以需要你手动创建;否则,MongoDB 将无法运行并抛出错误消息。此外,确保正确设置权限:MongoDB 必须具有读、写和目录创建权限才能正常工作。
如果您希望使用除了/data/db或C:\data\db之外的目录,那么您可以在执行服务时通过使用--dbpath标志来告诉 MongoDB 查看所需的目录。
一旦创建了所需的目录并分配了适当的权限,就可以通过执行 mongod 应用来启动 MongoDB 核心数据库服务。您可以分别从 Windows 和 Linux 中的命令提示符或 shell 中完成此操作。
测量安装布局
在成功安装或解压 MongoDB 之后,您将在bin目录中获得表 2-1 中所示的应用(在 Linux 和 Windows 中)。
表 2-1。
The Included MongoDB Applications
| 应用 | 功能 | | --- | --- | | `-- bsondump` | 读取 BSON 格式的回滚文件的内容。 | | `-- mongo` | 数据库 Shell。 | | `-- mongod` | 核心数据库服务器。 | | `-- mongodump` | 数据库备份实用程序。 | | `-- mongoexport` | 导出实用程序(JSON、CSV、TSV),对于备份不可靠。 | | `-- mongofiles` | 操作 GridFS 对象中的文件。 | | `-- mongoimport` | 导入实用程序(JSON、CSV、TSV),对于恢复不可靠。 | | `-- mongooplog` | 从另一个`mongod`实例中提取操作日志条目。 | | `-- mongoperf` | 检查磁盘 I/O 性能。 | | `--mongorestore` | 数据库备份恢复实用程序。 | | `--mongos` | Mongodb 分片过程。 | | `--mongosniff` | 实时嗅探/跟踪 MongoDB 数据库活动,仅限于类 Unix 系统。 | | `--mongostat` | 返回数据库操作的计数器。 | | `--mongotop` | 跟踪/报告 MongoDB 读/写活动。 | | `-- mongorestore` | 恢复/导入实用程序。 |Note: All applications are within the --bin directory.
安装的软件包括 15 个应用(在 Microsoft Windows 下是 14 个),它们将与您的 MongoDB 数据库一起使用。两个“最重要”的应用是mongo和mongod应用。mongo应用允许您使用数据库 Shell;这个 shell 使您能够用 MongoDB 完成几乎任何您想做的事情。
mongod应用启动服务或守护程序,也就是所谓的守护程序。在启动 MongoDB 应用时,还可以设置许多标志。例如,该服务让您指定数据库所在的路径(--dbpath),显示版本信息(--version,甚至打印一些诊断系统信息(带有--sysinfo标志)!启动服务时,您可以通过包含--help标志来查看选项的完整列表。现在,您可以使用默认设置,通过在 shell 或命令提示符下键入mongod来启动服务。
使用 MongoDB Shell
一旦创建了数据库目录并成功启动了 mongod 数据库应用,您就可以启动 shell 并体验 MongoDB 的强大功能了。
启动你的 shell (Unix)或命令提示符(Windows);当您这样做时,请确保您处于正确的位置,以便可以找到 mongo 可执行文件。您可以通过在命令prompt处键入mongo并按回车键来启动 shell。你将立即看到一个空白窗口和一个闪烁的光标(见图 2-1 )。女士们先生们,欢迎来到 MongoDB!
如果您使用默认参数启动 MongoDB 服务,并使用默认设置启动 shell,您将连接到本地主机上运行的默认test数据库。这个数据库是在您连接到它时自动创建的。这是 MongoDB 最强大的特性之一:如果你试图连接一个不存在的数据库,MongoDB 会自动为你创建一个。这可能是好的也可能是坏的,取决于你如何处理你的键盘。
Tip
MongoDB 网站上有一个在线演示 shell,您可以在这里试用列出的任何命令。

图 2-1。
The MongoDB shell
在采取任何进一步的步骤之前,比如实现任何使您能够使用您最喜欢的编程语言的额外驱动程序,您可能会发现快速浏览一下 MongoDB shell 中一些更有用的命令是有帮助的(参见表 2-2 )。
表 2-2。
Basic Commands within the MongoDB Shell
| 命令 | 功能 | | --- | --- | | `show dbs` | 显示可用数据库的名称。 | | `show collections` | 显示当前数据库中的集合。 | | `show users` | 显示当前数据库中的用户。 | | `useTip
您可以通过在 MongoDB shell 中键入help命令来获得完整的命令列表。
安装附加驱动程序
您可能认为既然已经设置了 MongoDB 并知道如何使用它的 shell,就已经准备好面对这个世界了。这部分是对的;但是,在查询或操作 MongoDB 数据库时,您可能希望使用自己喜欢的编程语言,而不是 shell。10gen 提供了多种官方驱动程序,社区中还提供了更多的驱动程序,让您可以准确地做到这一点。例如,可以在 MongoDB 网站上找到以下编程语言的驱动程序:
- C
- C++
- C#
- 占线小时
- 爪哇
- Java Script 语言
- Node.js
- Perl 语言
- 服务器端编程语言(Professional Hypertext Preprocessor 的缩写)
- 计算机编程语言
- 红宝石
- 斯卡拉
在这一节中,您将学习如何实现对目前使用的两种更流行的编程语言的 MongoDB 支持:PHP 和 Python。
Tip
有许多社区驱动的 MongoDB 驱动程序可用。在 MongoDB 网站上可以找到一长串的名单, www.mongodb.org 。
安装 PHP 驱动程序
PHP 是当今最流行的编程语言之一。这种语言专门针对 web 开发,可以很容易地集成到 HTML 中。这个事实使得这种语言成为设计 web 应用的完美候选语言,比如博客、留言簿,甚至是名片数据库。接下来的几节将介绍安装和使用 MongoDB PHP 驱动程序的选项。
为 PHP 获取 MongoDB
和 MongoDB 一样,PHP 也是一个跨平台的开发工具,在 PHP 中设置 MongoDB 所需的步骤根据目标平台的不同而不同。之前,本章向您展示了如何在 Ubuntu 和 Windows 上安装 MongoDB 这里我们将采用相同的方法,演示如何在 Ubuntu 和 Windows 上安装 PHP 驱动程序。
首先为你的操作系统下载 PHP 驱动程序。打开浏览器,导航至 www.mongodb.org 即可。在撰写本文时,该网站包括一个名为“驱动程序”的单独菜单选项。点击该选项,调出当前可用语言驱动列表(见图 2-2 )。
接下来,从语言列表中选择 PHP,并按照链接下载最新(稳定)版本的驱动程序。不同的操作系统需要不同的方法来自动安装 PHP 的 MongoDB 扩展。没错;正如您能够在 Ubuntu 上自动安装 MongoDB 一样,您也可以对 PHP 驱动程序进行同样的操作。而且就像在 Ubuntu 下安装 MongoDB 的时候,也可以选择手动安装 PHP 语言驱动。让我们看看你有两个选择。

图 2-2。
A short list of currently available language drivers for MongoDB
在基于 Unix 的平台上自动安装 PHP 驱动程序
PHP 开发者提出了一个很棒的解决方案,允许你用其他流行的扩展来扩展你的 PHP 安装:PECL。PECL 是一个专门为 PHP 设计的库;它提供了一个包含所有已知扩展的目录,可以用来下载、安装甚至开发 PHP 扩展。如果你已经熟悉了名为 aptitude 的包管理系统(你以前用它来安装 MongoDB),那么你会对 PECL 的界面与 aptitude 中的界面如此相似而感到高兴。
假设您的系统上安装了 PECL,打开一个控制台并输入以下命令来安装 MongoDB 扩展:
$ sudo pecl install mongo
输入这个命令会导致 PECL 自动下载并安装 PHP 的 MongoDB 扩展。换句话说,PECL 将下载你的 PHP 版本的扩展,并把它放在 PHP 扩展目录中。只有一个问题:PECL 不会自动将扩展添加到加载的扩展列表中;您需要手动执行此步骤。为此,打开一个文本编辑器(vim、nano 或您喜欢的任何文本编辑器)并修改名为php.ini的文件,这是 PHP 用来控制其行为的主要配置文件,包括它应该加载的扩展。
接下来,打开php.ini文件,向下滚动到 extensions 部分,添加下面一行来告诉 PHP 加载 MongoDB 驱动程序:
extension=mongo.so
Note
前面的步骤是强制性的;如果不这样做,PHP 中的 MongoDB 命令将无法运行。要在您的系统上找到php.ini文件,您可以在您的 shell 中使用grep命令:php –i | grep Configuration。
本章后面的“确认您的 PHP 安装工作正常”一节将介绍如何确认一个扩展已经成功加载。
就这些了,伙计们!您已经为 PHP 安装安装了 MongoDB 扩展,现在可以使用它了。接下来,您将学习如何手动安装驱动程序。
在基于 Unix 的平台上手动安装 PHP 驱动程序
如果您更喜欢自己编译驱动程序,或者由于某种原因无法使用前面描述的 PECL 应用(例如,您的主机提供商可能不支持此选项),那么您也可以选择下载源驱动程序并手动编译。
要下载驱动程序,请访问 github 网站( http://github.com )。这个网站提供了 PHP 驱动程序的最新源码包。下载后,您需要解压缩该包,并通过运行以下命令集来创建驱动程序:
$ tar zxvf mongodb-mongdb-php-driver-<commit_id>.tar.gz
$ cd mongodb-mongodb-php-driver-<commit_id>
$ phpize
$ ./configure
$ sudo make install
这个过程可能需要一段时间,具体取决于系统的速度。一旦这个过程完成,您的 MongoDB PHP 驱动程序就安装好了,可以使用了!在您执行命令之后,您将看到驱动程序被放置的位置;通常,输出如下所示:
Installing '/ usr/lib/php/extensions/no-debug-zts-20060613/mongo.so'
您确实需要确认这个目录是 PHP 默认存储其扩展的同一个目录。您可以使用以下命令来确认 PHP 存储其扩展的位置:
$ php -i | grep extension_dir
这一行输出应该放置所有 PHP 扩展的目录。如果这个目录与放置mongo.so驱动程序的目录不匹配,那么您必须将mongo.so驱动程序移动到正确的目录,这样 PHP 就知道在哪里可以找到它。
和以前一样,您需要告诉 PHP 新创建的扩展已经放在它的扩展目录中,并且它应该加载这个扩展。您可以通过修改php.ini文件的扩展名部分来指定这一点;将下面一行添加到该部分:
extension=mongo.so
最后,需要重新启动您的 web 服务。当使用 Apache HTTPd 服务时,您可以使用以下服务命令来完成此操作:
sudo /etc/init.d/apache2 restart
就这样!这个过程比使用 PECL 的自动化方法稍长一些;但是,如果您无法使用 PECL,或者如果您是一名驱动程序开发者,并且对错误修复感兴趣,那么您可能希望使用手动方法。
在 Windows 上安装 PHP 驱动程序
您之前已经了解了如何在 Windows 操作系统上安装 MongoDB。现在让我们看看如何在 Windows 上实现 PHP 的 MongoDB 驱动程序。
对于 Windows,MongoDB 的 PHP 驱动程序的每个版本都有预编译的二进制文件。你可以从前面提到的 github 网站( http://github.com )获得这些二进制文件。这种情况下最大的挑战是为您的 PHP 版本选择正确的安装包(有各种各样的包可供选择)。如果您不确定您需要哪个包版本,您可以在 PHP 页面中使用<? phpinfo(); ?>命令来确切地了解哪个适合您的特定环境。在下一节中,我们将仔细看看phpinfo()命令。
下载正确的包并提取其内容后,你需要做的就是将驱动文件(名为php_mongo.dll)复制到你的 PHP 的扩展目录下;这使得 PHP 能够获得它。
根据您的 PHP 版本,扩展目录可能被称为Ext或Extensions。如果您不确定应该是哪个目录,您可以查看系统上安装的 PHP 版本附带的 PHP 文档。
一旦将驱动程序 DLL 放入 PHP 扩展目录,您仍然需要告诉 PHP 加载驱动程序。通过修改php.ini文件并在扩展部分添加以下行来实现这一点:
extension=php_mongo.dll
完成后,重启系统上的 HTTP 服务,现在就可以在 PHP 中使用 MongoDB 驱动程序了。然而,在开始将 MongoDB 的魔力用于 PHP 之前,您需要确认扩展被正确加载。
确认您的 PHP 安装工作正常
到目前为止,您已经成功地在 PHP 中安装了 MongoDB 和 MongoDB 驱动程序。现在是时候做一个快速检查来确认驱动程序是否被 PHP 正确加载了。PHP 为您提供了一个简单明了的方法来完成这个任务:phpinfo()命令。这个命令显示了所有加载的模块的扩展概述,包括版本号、编译选项、服务器信息、操作系统信息等等。
要使用phpinfo()命令,打开一个文本或 HTML 编辑器并键入以下内容:
<? phpinfo(); ?>
接下来,将文档保存在 web 服务器的www目录中,并随意命名。例如,你可以称它为test.php或phpinfo.php。现在打开浏览器,转到您的本地主机或外部服务器(也就是说,转到您正在使用的任何服务器),查看您刚刚创建的页面。您将看到所有 PHP 组件和各种其他相关信息的概述。这里需要关注的是显示 MongoDB 信息的部分。该部分将列出版本号、端口号、主机名等(见图 2-3 )。
一旦您确认安装成功并且驱动程序加载成功,您就可以编写一些 PHP 代码并浏览一个利用 PHP 的 MongoDB 示例了。

图 2-3。
Displaying your MongoDB information in PHP
连接和断开 PHP 驱动程序
您已经确认 MongoDB PHP 驱动程序已经正确加载,所以是时候开始编写一些 PHP 代码了!让我们来看看使用 MongoDB 的两个简单而基本的选项:启动 MongoDB 和 PHP 之间的连接,然后切断该连接。
您使用Mongo类来启动 MongoDB 和 PHP 之间的连接;这个类还允许您使用数据库服务器命令。一个简单而典型的连接命令如下所示:
$connection = new Mongo();
如果使用这个命令而不提供任何参数,它将连接到本地主机上默认 MongoDB 端口(27017)上的 MongoDB 服务。如果您的 MongoDB 服务正在其他地方运行,那么您只需指定想要连接的远程主机的主机名:
$connection = new Mongo("example.com
这一行为您的 MongoDB 服务实例化了一个新的连接,该服务运行在服务器上并监听example.com域名(注意,它仍将连接到默认端口:27017)。但是,如果您想要连接到不同的端口号(例如,如果您不想使用默认端口,或者您已经在该端口上运行了另一个 MongoDB 服务会话),您可以通过指定端口号和主机名来实现:
$connection = new Mongo("example.com:12345
此示例创建了一个到数据库服务的连接。接下来,您将学习如何断开与服务的连接。假设您使用刚才描述的方法连接到数据库,您可以再次调用$connection来传递close()命令以终止连接,如下例所示:
$connection->close();
除非在特殊情况下,否则不需要结束。原因是一旦Mongo对象超出范围,PHP 驱动程序就会关闭与数据库的连接。尽管如此,建议您在 PHP 代码的末尾调用close();这有助于您避免让旧连接徘徊不前,直到它们最终超时。它还可以帮助您确保任何现有的连接都已关闭,从而允许新的连接发生,如下例所示:
$connection = new Mongo();
$connection->close();
$connection->connect();
下面的代码片段显示了它在 PHP 中的样子:
<?php
// Establish the database connection
$connection = new Mongo()
// Close the database connection
$connection->close();
?>
安装 Python 驱动程序
Python 是一种通用且易读的编程语言。
这些品质使 Python 成为您初学编程和脚本时的一种好的入门语言。如果您熟悉编程,并且您正在寻找一种允许多种编程风格(面向对象编程、结构化编程等)的多范式编程语言,那么它也是一种很好的语言。在接下来的小节中,您将学习如何安装 Python 并启用对该语言的 MongoDB 支持。
在 Linux 下安装 PyMongo
Python 为 MongoDB 支持提供了一个名为 PyMongo 的特定包。这个包允许您与 MongoDB 数据库进行交互,但是在使用这个强大的组合之前,您需要启动并运行这个驱动程序。与安装 PHP 驱动程序一样,有两种方法可以用来安装 PyMongo:一种是依赖 setuptools 的自动化方法,另一种是下载项目源代码的手动方法。下面几节将向您展示如何使用这两种方法安装 PyMongo。
自动安装 PyMongo
与python-pip包捆绑在一起的pip应用允许您自动下载、构建、安装和管理 Python 包。这非常方便,使您能够扩展 Python 模块的安装,即使它为您做了所有的工作。
Note
您必须安装 setuptools 才能使用pip应用。这将在安装 python-pip 包时自动完成。
要安装pip,你需要做的就是告诉apt-get下载并安装它,就像这样:
$ sudo apt-get install python-pip
当这一行执行时,pip将检测当前运行的 Python 版本,并将其自身安装到系统上。这就是全部了。现在您已经准备好使用pip命令来下载、制作和安装 MongoDB 模块,如下例所示:
$ sudo pip install pymongo
再说一遍,这就是全部了!PyMongo 现在已经安装完毕,可以使用了。
Tip
您也可以使用pip install pymongo= x.y.z命令用 pip 安装 PyMongo 模块的以前版本。这里,x.y.z表示模块的版本。
手动安装 PyMongo
也可以选择手动安装 PyMongo。首先转到托管 PyMongo 插件的站点的下载部分( http://pypi.python.org/pypi/pymongo )。接下来,下载 tarball 并提取它。在您的控制台中,典型的下载和提取过程可能如下所示:
$ wgethttp://pypi.python.org/packages/source/p/pymongo/pymongo-2.5.1.tar.gz
$ tar xzf pymongo-2.5.1.tar.gz
一旦您成功下载并提取了这个文件,就可以进入提取的内容目录,并通过使用 Python 运行install.py命令来调用 PyMongo 的安装:
$ cd pymongo-2.5.1
$ sudo python setup.py install
前面的代码片段输出了 PyMongo 模块的整个创建和安装过程。最终,这个过程会将您带回到提示符处,这时您就可以开始使用 PyMongo 了。
在 Windows 下安装 PyMongo
在 Windows 下安装 PyMongo 是一个简单的过程。和在 Linux 下安装 PyMongo 一样,Easy Install 也可以简化在 Windows 下安装 PyMongo。如果你还没有安装 setuptools(这个包包括easy_install命令),那么去 Python 包索引网站( http://pypi.python.org )定位 setuptools 安装程序。
Caution
您下载的 setuptools 版本必须与您系统上安装的 Python 版本相匹配。
例如,假设您的系统上安装了 Python 版本 2.7.5。您将需要下载 2.7.x 版的 setuptools 包,好消息是您不需要编译这些;相反,您可以简单地下载适当的软件包,然后双击可执行文件在您的系统上安装 setuptools!就这么简单。
Caution
如果您之前已经安装了旧版本的 setuptools,那么在安装新版本之前,您需要使用系统的“添加/删除程序”功能卸载该版本。
一旦安装完成,您将在 Python 的脚本子目录中找到easy_install.exe文件。至此,您已经准备好在 Windows 上安装 PyMongo 了。
一旦你成功安装了 setuptools,你就可以打开一个命令提示符并进入 Python 的脚本目录。默认设置为C:\Python xy \Scripts\,其中 xy 代表您的版本号。导航到此位置后,您可以使用前面显示的相同语法来安装 Unix 变体:
C:\Python27\Scripts> easy_install PyMongo
与在 Linux 机器上安装该程序时得到的输出不同,这里的输出相当简短,仅表示已经下载并安装了扩展(参见图 2-4 )。也就是说,在这种情况下,这些信息足以满足您的目的。

图 2-4。
Installing PyMongo under Windows
确认您的 PyMongo 安装可以工作
要确认 PyMongo 安装是否成功完成,可以打开 Python shell。在 Linux 中,你可以通过打开一个控制台并输入python来实现。在 Windows 中,您可以通过单击开始➤程序➤ Python xy ➤ Python(命令行)来完成此操作。此时,你将被欢迎进入 Python 的世界(见图 2-5 )。

图 2-5。
The Python shell
您可以使用import命令告诉 Python 开始使用新安装的扩展:
>>> import pymongo
>>>
Note
每次想要使用 PyMongo 时,都必须使用import pymongo命令。
如果一切顺利,您将什么也看不到,并且可以开始使用一些奇特的 MongoDB 命令。但是,如果您收到一条错误消息,那么一定是出错了,您可能需要回顾刚刚采取的步骤,以发现错误发生在哪里。
摘要
在本章中,我们研究了如何获得 MongoDB 软件,包括如何为您的环境选择正确的版本。我们还讨论了版本号、如何安装和运行 MongoDB,以及如何安装和运行它的先决条件。接下来,我们讲述了如何通过 shell、PHP 和 Python 的组合建立到数据库的连接。
我们还探讨了如何扩展 MongoDB,以便它可以与您喜欢的编程语言一起工作,以及如何确认特定于语言的驱动程序是否已经正确安装。
在下一章中,我们将探讨如何正确地设计和构造 MongoDB 数据库和数据。在这个过程中,您将学习如何索引信息以加快查询速度,如何引用数据,以及如何利用一个称为地理空间索引的奇特新功能。
三、数据模型
Abstract
在前一章中,您学习了如何在两个常用的平台(Windows 和 Linux)上安装 MongoDB,以及如何用一些额外的驱动程序来扩展数据库。在本章中,您将把注意力从操作系统上转移,转而研究 MongoDB 数据库的总体设计。具体来说,您将了解什么是集合,文档看起来像什么,索引如何工作以及它们做什么,最后,何时何地引用数据而不是嵌入数据。我们在第一章中简单地提到了一些概念,但是在这一章中,我们将更详细地探讨它们。在这一章中,你会看到一些代码示例,它们旨在让你对所讨论的概念有一个良好的感觉。不过,不要太担心你将看到的命令,因为它们将在第 4 章中详细讨论。
在前一章中,您学习了如何在两个常用的平台(Windows 和 Linux)上安装 MongoDB,以及如何用一些额外的驱动程序来扩展数据库。在本章中,您将把注意力从操作系统上转移,转而研究 MongoDB 数据库的总体设计。具体来说,您将了解什么是集合,文档看起来像什么,索引如何工作以及它们做什么,最后,何时何地引用数据而不是嵌入数据。我们在第 1 章中简要地提到了其中的一些概念,但是在这一章中,我们将更详细地探讨它们。在这一章中,你会看到一些代码示例,它们旨在让你对所讨论的概念有一个良好的感觉。不过,不要太担心你将看到的命令,因为它们将在第四章中被广泛讨论。
设计数据库
正如您在前两章中了解到的,MongoDB 数据库是无关系和无模式的。这意味着 MongoDB 数据库不像关系数据库(如 MySQL)那样绑定到任何预定义的列或数据类型。这种实现的最大好处是处理数据非常灵活,因为文档中不需要预定义的结构。
更简单地说:您完全可以拥有一个包含数百甚至数千个文档的集合,这些文档都具有不同的结构——而不违反任何 MongoDB 数据库规则。
这种灵活的无模式设计的好处之一是,在用 Python 或 PHP 等动态类型语言编程时,您不会受到限制。事实上,如果由于数据库的先天限制,您的极其灵活且具有动态能力的编程语言不能发挥其全部潜力,这将是一个严重的限制。
让我们再看一下 MongoDB 中文档的数据设计是什么样子,特别注意 MongoDB 中的数据与关系数据库中的数据相比有多灵活。在 MongoDB 中,文档是包含实际数据的项,相当于 SQL 中的一行。在下面的例子中,您将看到两种完全不同类型的文档如何在一个名为Media的集合中共存(注意,集合大致相当于 SQL 世界中的一个表):
{
"Type": "CD",
"Artist": "Nirvana",
"Title": "Nevermind",
"Genre": "Grunge",
"Releasedate": "1991.09.24",
"Tracklist": [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Length" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
]
}
{
"type": "Book",
"Title": "Definitive Guide to MongoDB: A complete guide to dealing with Big Data using MongoDB 2nd, The",
"ISBN": "987-1-4302-5821-6",
"Publisher": "Apress",
"Author": [
"Hows, David"
"Plugge, Eelco",
"Membrey, Peter",
"Hawkins, Tim]
}
在查看这两个文档时,您可能已经注意到,大多数字段彼此并不紧密相关。是的,它们都有名为Title和Type的字段;但是除了相似性之外,这些文档是完全不同的。尽管如此,这两个文件都包含在一个叫做Media的集合中。
MongoDB 被称为无模式数据库,但这并不意味着 MongoDB 的数据结构完全没有模式。例如,您确实在 MongoDB 中定义了集合和索引(您将在本章后面了解更多)。然而,您不需要为将要添加的任何文档预定义一个结构,例如使用 MySQL 时就是这样。
简单地说,MongoDB 是一个非常动态的数据库;除非您也将每个可能的字段添加到您的表中,否则前面的示例在关系数据库中永远不会起作用。这样做将会浪费空间和性能,更不用说高度混乱了。
向下钻取集合
如前所述,集合是 MongoDB 中常用的术语。你可以把一个集合想象成一个存储你的文档(也就是你的数据)的容器,如图 3-1 所示。

图 3-1。
The MongoDB database model
现在将 MongoDB 数据库模型与关系数据库的典型模型进行比较(参见图 3-2 )。

图 3-2。
A typical relational database model
正如您所看到的,这两种类型的数据库的一般结构是相同的;然而,你不会以一点相似的方式使用它们。MongoDB 中有几种类型的集合。默认集合类型的大小是可扩展的:添加的数据越多,它就越大。也可以定义有上限的集合。在最旧的文档被较新的文档替换之前,这些有上限的集合只能包含一定量的数据(您将在第 4 章中了解关于这些集合的更多信息)。
MongoDB 中的每个集合都有一个唯一的名称。当使用createCollection函数创建时,该名称应该以字母开头,或者可选地以下划线(_)开头。名称可以包含数字和字母;然而,$符号是由 MongoDB 保留的。同样,不允许使用空字符串(" ");名称中不能使用空字符,并且不能以“system开头字符串。一般来说,建议您保持集合的名称简单而简短(大约九个字符左右);但是,集合名称中允许的最大字符数是 128。显然,没有太多实际的理由来创造这么长的名字。
单个数据库的默认限制是 24,000 个名称空间。每个集合至少有两个名称空间:一个用于集合本身,另一个用于在集合中创建的第一个索引。但是,如果要为每个集合添加更多的索引,就需要使用另一个名称空间。理论上,这意味着默认情况下每个数据库最多可以有 12,000 个集合,假设每个集合只包含一个索引。但是,在执行 MongoDB 服务应用(mongod)时,可以通过提供nssize参数来增加对名称空间数量的限制。
使用文档
回想一下,文档由键值对组成。例如,对"type" : "Book"由名为type的键和它的值Book组成。键被写成字符串,但是其中的值可以有很大的不同。值可以是一组丰富的数据类型中的任何一种,例如数组,甚至是二进制数据。记住:MongoDB 以 BSON 格式存储它的数据(参见第 1 章了解关于这个主题的更多信息)。
接下来,让我们看看可以添加到文档中的所有可能的数据类型,以及它们的用途:
- String:这种常用的数据类型包含一个文本字符串(或任何其他类型的字符)。该数据类型主要用于存储文本值(例如,
"Country" : "Japan")。 - Integer (32b 和 64b):该类型用于存储一个数值(例如,
{ "Rank" : 1 })。请注意,整数前后没有引号。 - Boolean:该数据类型可以设置为
TRUE或FALSE。 - Double:该数据类型用于存储浮点值。
- 最小/最大键:该数据类型用于将一个值分别与最低和最高 BSON 元素进行比较。
- 数组:该数据类型用于存储数组(例如,[
"Membrey, Peter","Plugge, Eelco","Hows, David"])。 - 时间戳:该数据类型用于存储时间戳。当文档被修改或添加时,这对于记录非常方便。
- 对象:该数据类型用于嵌入的文档。
- Null:该数据类型用于一个
Null值。 - 符号:该数据类型的用法与字符串相同;然而,它通常是为使用特定符号类型的语言保留的。
- Date *:该数据类型用于以 Unix 时间格式(POSIX 时间)存储当前日期或时间。
- Object ID *:该数据类型用于存储文档的 ID。
- 二进制数据*:该数据类型用于存储二进制数据。
- 正则表达式*:该数据类型用于正则表达式。所有选项都由按字母顺序提供的特定字符表示。你会在第 4 章学到更多关于正则表达式的知识。
- JavaScript 代码*:该数据类型用于 JavaScript 代码。
星号意味着最后五种数据类型(日期、对象 ID、二进制数据、正则表达式和 JavaScript 代码)是非 JSON 类型;具体来说,它们是 BSON 允许您使用的特殊数据类型。在第 4 章中,你将学习如何使用$type操作符来识别你的数据类型。
理论上,这听起来很简单。然而,您可能想知道如何实际设计文档,包括在其中放入什么信息。因为文档可以包含任何类型的数据,所以您可能认为没有必要从另一个文档中引用信息。在下一节中,我们将研究在文档中嵌入信息与从另一个文档中引用信息相比的优缺点。
在文档中嵌入和引用信息
您可以选择将信息嵌入到文档中,或者从另一个文档中引用该信息。嵌入信息仅仅意味着将某种类型的数据(例如,包含更多数据的数组)放入文档本身。引用信息意味着创建对包含该特定数据的另一个文档的引用。通常,在使用关系数据库时会引用信息。例如,假设您想使用一个关系数据库来跟踪您的 CD、DVD 和书籍。在这个数据库中,您可能有一个用于 CD 收藏的表和另一个用于存储 CD 曲目列表的表。因此,您可能需要查询多个表来获取特定 CD 上的曲目列表。
然而,使用 MongoDB(和其他非关系数据库),嵌入这样的信息会容易得多。毕竟,文档本身就能够做到这一点。采用这种方法可以使数据库保持整洁,确保所有相关信息都保存在一个文档中,甚至可以更快地工作,因为数据随后会存放在磁盘上。
现在让我们通过一个真实的场景来看看嵌入和引用信息之间的区别:将 CD 数据存储在数据库中。
在关系方法中,您的数据结构可能如下所示:
|_media
|_cds
|_id, artist, title, genre, releasedate
|_ cd_tracklists
|_cd_id, songtitle, length
在非关系方法中,您的数据结构可能如下所示:
|_media
|_items
|_<document>
在非关系方法中,文档可能如下所示:
{
"Type": "CD",
"Artist": "Nirvana",
"Title": "Nevermind",
"Genre": "Grunge",
"Releasedate": "1991.09.24",
"Tracklist": [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Length" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
]
}
在这个例子中,曲目列表信息被嵌入在文档本身中。这种方法既高效又组织有序。您希望存储的关于此 CD 的所有信息都被添加到一个文档中。在 CD 数据库的关系版本中,这至少需要两个表;在非关系数据库中,只需要一个集合和一个文档。
当检索给定 CD 的信息时,只需要将该信息从一个文档加载到 RAM 中,而不是从多个文档中。请记住,每个引用都需要数据库中的另一个查询。
Tip
使用 MongoDB 的经验法则是尽可能嵌入数据。这种方法效率更高,而且几乎总是可行的。
此时,您可能想知道一个应用有多个用户的用例。一般来说,前面提到的 CD 应用的关系数据库版本需要一个包含所有用户的表和两个添加条目的表。对于非关系数据库来说,为用户和添加的项建立单独的集合是一个很好的实践。对于这类问题,MongoDB 允许您以两种方式创建引用:手动或自动。在后一种情况下,您使用 DBRef 规范,它提供了更大的灵活性,以防集合从一个文档变化到下一个文档。你将在第 4 章中了解更多关于这两种方法的信息。
创建 _id 字段
MongoDB 数据库中的每个对象都包含一个惟一的标识符,用于将该对象与其他所有对象区分开来。这个标识符称为 _ id键,它会自动添加到您在集合中创建的每个文档中。
_id键是您创建的每个新文档中添加的第一个属性。即使您不告诉 MongoDB 创建密钥,这一点仍然成立。例如,前面例子中的代码都没有使用_id键。尽管如此,MongoDB 会在每个文档中自动为您创建一个_id键。这样做是因为_id键是集合中每个文档的强制元素。
如果不手动指定_id值,该类型将被设置为由 12 字节二进制值组成的特殊 BSON 数据类型。由于它的设计,这个值很有可能是唯一的。12 字节的值由一个 4 字节的时间戳(从纪元开始的秒数,或 1970 年 1 月 1 日 st )、一个 3 字节的机器 ID、一个 2 字节的进程 ID 和一个 3 字节的计数器组成。很高兴知道counter和时间戳字段是以大端格式存储的。这是因为 MongoDB 希望确保这些值的顺序是递增的,而 Big Endian 方法最适合这一要求。
Note
术语大端序和小端序指的是单个字节/位如何存储在存储器中较长的数据字中。Big Endian 仅仅意味着首先保存最重要的值。类似地,Little Endian 意味着首先保存最不重要的值。
图 3-3 显示了_id键的值是如何建立的,以及这些值的来源。

图 3-3。
Creating the _id key in MongoDB
使用 MongoDB 时加载的每一个额外支持的驱动程序(如 PHP 驱动程序或 Python 驱动程序)都支持这种特殊的 BSON 数据类型,并在创建新数据时使用它。您还可以从 MongoDB shell 调用ObjectId()来为_id键创建一个值。或者,您可以使用ObjectId( string )指定自己的值,其中string代表指定的十六进制字符串。
构建索引
正如在第一章中提到的,索引只不过是一种数据结构,它收集关于集合文档中指定字段值的信息。MongoDB 的查询优化器使用这种数据结构对集合中的文档进行快速排序。
请记住,索引可以确保快速查找文档中的数据。基本上,您应该将索引视为预定义的查询,该查询已被执行并存储了其结果。可以想象,这极大地提高了查询性能。MongoDB 的一般经验是,您应该为您希望在 MySQL 中拥有索引的同类场景创建一个索引。
创建自己的索引的最大好处是查询经常使用的信息会非常快,因为您的查询不需要遍历整个数据库来收集这些信息。
创建(或删除)索引相对容易——只要你掌握了窍门。您将在第 4 章中学习如何操作,该章涵盖了数据处理。在第 1 章 0 中,您还将学习一些更高级的利用索引的技术,其中涵盖了如何最大化性能。
索引对性能的影响
您可能想知道为什么需要删除索引、重建索引,甚至删除集合中的所有索引。简单的答案是,这样做可以让你清理一些违规行为。例如,有时数据库的大小会莫名其妙地急剧增加。在其他时候,索引占用的空间可能会让您觉得过多。
需要记住的另一件好事是:每个集合最多可以有 40 个索引。一般来说,这比你应该需要的要多得多,但是有一天你可能会达到这个极限。
Note
添加索引可以提高查询速度,但会降低插入或删除速度。最好只考虑为读取次数高于写入次数的集合添加索引。当写操作多于读操作时,索引甚至会适得其反。
最后,所有索引信息都存储在数据库的system.indexes集合中。例如,您可以运行db.indexes.find()命令来快速查看到目前为止已经存储的索引。要查看为特定集合创建的索引,可以使用getIndexes命令:
db.collection.getIndexes()
实施地理空间索引
正如第 1 章简要提到的,MongoDB 从 1.4 版本开始就实现了地理空间索引。这意味着,除了普通索引之外,MongoDB 还支持地理空间索引,这些索引旨在以最佳方式处理基于位置的查询。例如,您可以使用此功能来查找与用户当前位置最近的已知项目。或者,您可以进一步细化搜索,以查询当前位置附近指定数量的餐馆。如果您正在设计一个应用,希望找到离给定客户的邮政编码最近的分支机构,这种类型的查询会特别有用。
要为其添加地理空间信息的文档必须包含子对象或数组,其第一个元素指定对象类型,后跟项目的经度和纬度,如下例所示:
> db.restaurants.insert({name: "Kimono", loc: { type: "Point", coordinates: [ 52.37045
1, 5.217497]}})
注意,type参数可以用来指定文档的对象类型,可以是Point、LineString或Polygon。正如所料,Point类型用于指定项目(在本例中是一家餐馆)正好位于给定的地点,因此正好需要两个值,经度和纬度。LineString类型可用于指定项目沿着特定的线(比如街道)延伸,因此需要起点和终点,如下例所示:
> db.streets.insert( {name: "Westblaak", loc: { type: "LineString", coordinates: [ [52.36881,4.890286],[52.368762,4.890021] ] } })
类型可以用来指定一个(非默认的)形状(比如一个购物区)。使用此类型时,您需要确保第一个点和最后一个点是相同的,以闭合回路。此外,点坐标将作为数组中的数组提供,如下例所示:
> db.stores.insert( {name: "SuperMall", loc: { type: "Polygon", coordinates: [ [ [52.146917,5.374337], [52.146966,5.375471], [52.146722,5.375085], [52.146744,5.37437], [52.146917,5.374337] ] ] } } )
在大多数情况下,Point类型是合适的。
一旦地理空间信息被添加到文档中,您就可以创建索引(当然,甚至可以预先创建索引)并为ensureIndex()函数提供2dsphere参数:
> db.restaurants.ensureIndex( { loc: "2dsphere" } )
Note
ensureIndex()功能用于添加自定义索引。先不要担心这个函数的语法——你将在下一章深入学习如何使用ensureIndex()。
2dsphere参数告诉ensureIndex()它正在索引一个类似地球的球体上的坐标或一些其他形式的二维信息。默认情况下,ensureindex()假设给出了一个纬度/经度键,它使用的范围是从-180到180。但是,您可以使用min和max参数覆盖这些值:
> db.restaurants.ensureIndex( { loc: "2dsphere" }, { min : -500 , max : 500 } )
还可以通过使用次键值(也称为复合键)来扩展地理空间索引。当您打算查询多个值时,此结构会很有用,例如位置(地理空间信息)和类别(升序排序):
> db.restaurants.ensureIndex( { loc: "2dsphere", category: 1 } )
Note
此时,地理空间实现是基于世界是一个完美球体的想法。因此,每一度的纬度和经度正好是 111 公里(69 英里)长。然而,这只有在赤道上才是正确的;离赤道越远,经度的每一度变得越小,在两极接近零。
查询地理空间信息
在这一章中,我们主要关注两件事:如何对数据建模,以及数据库如何在应用的后台工作。也就是说,在各种各样的应用中,操作地理空间信息变得越来越重要,所以我们将花一些时间来解释如何在 MongoDB 数据库中利用地理空间信息。
在开始之前,一个温和的警告。如果您对 MongoDB 完全陌生,并且过去没有机会使用(地理空间)索引数据,这一部分可能会让您感到有些不知所措。不过,不用担心;你现在可以安全地跳过它,如果你愿意,以后再回来。给出的例子向您展示了如何(以及为什么)使用地理空间索引的实际例子,使其更容易理解。抛开这些不谈,如果你觉得自己很勇敢,请继续读下去。
一旦向集合中添加了数据,并且创建了索引,就可以进行地理空间查询。例如,让我们看几行简单而强大的代码,演示如何使用地理空间索引。
首先启动 MongoDB shell,用use函数选择一个数据库。在这种情况下,数据库被命名为restaurants:
使用餐厅
一旦选择了数据库,您就可以定义一些包含地理空间信息的文档,然后将它们插入到places集合中(记住:您不需要事先创建集合):
> db.restaurants.insert( { name: "Kimono", loc: { type: "Point", coordinates: [ 52.37045
1, 5.217497] } } )
> db.restaurants.insert( {name: "Shabu Shabu", loc: { type: "Point", coordinates: [51.9
15288,4.472786] } } )
> db.restaurants.insert( {name: "Tokyo Cafe", loc: { type: "Point", coordinates: [52.36
8736, 4.890530] } } )
添加数据后,您需要告诉 MongoDB shell 根据在loc键中指定的位置信息创建一个索引,如下例所示:
> db.restaurants.ensureIndex ( { loc: "2dsphere" } )
一旦创建了索引,您就可以开始搜索您的文档了。首先搜索一个精确的值(到目前为止,这是一个“普通的”查询;此时与地理空间信息无关):
> db.restaurants.find( { loc : [52,5] } )
>
前面的搜索没有返回任何结果。这是因为查询太具体了。在这种情况下,更好的方法是搜索包含接近给定值的信息的文档。您可以使用$near操作符来完成这个任务。请注意,这需要指定type运算符,如下例所示:
> db.restaurants.find( { loc : { $geoNear : { $geometry : { type : "Point", coordinates:
[52.338433,5.513629] } } } } )
这会产生以下输出:
{
"_id" : ObjectId("51ace0f380523d89efd199ac"),
"name" : "Kimono",
"loc" : {
"type" : "Point",
"coordinates" : [ 52.370451, 5.217497 ]
}
}
{
"_id" : ObjectId("51ace13380523d89efd199ae"),
"name" : "Tokyo Cafe",
"loc" : {
"type" : "Point",
"coordinates" : [ 52.368736, 4.89053 ]
}
}
{
"_id" : ObjectId("51ace11b80523d89efd199ad"),
"name" : "Shabu Shabu",
"loc" : {
"type" : "Point",
"coordinates" : [ 51.915288, 4.472786 ]
}
}
尽管这组结果看起来确实更好,但是仍然有一个问题:所有的文档都被返回了!当不使用任何附加运算符时,$near返回前 100 个条目,并根据它们与给定坐标的距离对它们进行排序。现在,虽然我们可以选择使用limit函数来限制我们的结果,比如说前两项(或者两百项,如果我们想要的话),但是更好的做法是将结果限制在给定范围内。
这可以通过添加$maxDistance操作符来实现。使用这个操作符,您可以告诉 MongoDB 只返回那些距离给定点最大距离(以米为单位)以内的结果,如下例及其输出所示:
> db.retaurants.find( { loc : { $geoNear : { $geometry : { type : "Point", coordinates: [52.338433,5.513629] }, $maxDistance : 40000 } } } )
{
"_id" : ObjectId("51ace0f380523d89efd199ac"),
"name" : "Kimono",
"loc" : {
"type" : "Point",
"coordinates" : [ 52.370451, 5.217497 ]
}
}
如您所见,这仅返回一个结果:一家位于距起点 40 公里(或大约 25 英里)以内的餐馆。
Note
返回结果的数量和给定查询的执行时间之间有直接的关系。
除了$geoNear操作符,MongoDB 还包括一个$geoWithin操作符。您可以使用此运算符来查找特定形状的项目。此时,您可以找到位于$box, $polygon, $center和$centerSphere形状中的项目,其中$box表示矩形,$polygon表示您选择的特定形状,$center表示圆形,$centerSphere定义球体上的圆。让我们看几个额外的例子来说明如何使用这些形状。
Note
在 MongoDB 的 2.4 版本中,$within操作符被弃用,取而代之的是$geoWithin。这个操作符并不严格要求地理空间索引。此外,与$near操作符不同,$geoWithin不对返回的结果进行排序,从而提高了它们的性能。
要使用$box形状,首先需要指定盒子的左下角坐标,然后是右上角坐标,如下例所示:
> db.restaurants.find( { loc: { $geoWithin : { $box : [ [52.368549,4.890238], [52.368849,4.89094] ] } } } )
类似地,要在特定的多边形表单中查找项目,需要将点的坐标指定为一组嵌套数组。再次注意,第一个和最后一个坐标必须相同,才能正确闭合形状,如下例所示:
> db.restaurants.find( { loc :
{ $geoWithin :
{ $geometry :
{ type : "Polygon",
coordinates : [ [
[52.368739,4.890203], [52.368872,4.890477], [52.368726,4.890793],
[52.368608,4.89049], [52.368739,4.890203]
] ]
}
}
} )
在基本的$circle形状中查找项目的代码非常简单。在这种情况下,在执行find()功能之前,您需要指定圆心及其半径,用坐标系使用的单位测量:
> db.restaurants.find( { loc: { $geoWithin : { $center : [ [52.370524, 5.217682], 10] } } } )
注意,从 MongoDB 版本 2.2.3 开始,$center操作符可以在没有地理空间索引的情况下使用。但是,建议创建一个以提高性能。
最后,要查找位于球体(比如我们的星球)上的圆形内的项目,可以使用$centerSphere操作符。该运算符类似于$center,比如:
> db.restaurants.find( { loc: { $geoWithin : { $centerSphere : [ [52.370524, 5.217682], 10] } } } )
默认情况下,find()函数非常适合运行查询。然而,MongoDB 还提供了geoNear()函数,它的工作方式类似于find()函数,但也显示结果中每个项目离指定点的距离。geoNear()功能还包括一些额外的诊断。以下示例使用geoNear()函数来查找最接近指定位置的两个结果:
> db.runCommand( { geoNear : "restaurants", near : { type : "Point", coordinates: [52.338433,5.513629] }, spherical : true})
它返回以下结果:
{
"ns" : "stores.restaurants",
"results" : [
{
"dis" : 33155.517810497055,
"obj" : {
"_id" : ObjectId("51ace0f380523d89efd199ac"),
"name" : "Kimono",
"loc" : {
"type" : "Point",
"coordinates" : [
52.370451,
5.217497
]
}
}
},
{
"dis" : 69443.96264213261,
"obj" : {
"_id" : ObjectId("51ace13380523d89efd199ae"),
"name" : "Tokyo Cafe",
"loc" : {
"type" : "Point",
"coordinates" : [
52.368736,
4.89053
]
}
}
},
{
"dis" : 125006.87383713324,
"obj" : {
"_id" : ObjectId("51ace11b80523d89efd199ad"),
"name" : "Shabu Shabu",
"loc" : {
"type" : "Point",
"coordinates" : [
51.915288,
4.472786
]
}
}
}
],
"stats" : {
"time" : 6,
"nscanned" : 3,
"avgDistance" : 75868.7847632543,
"maxDistance" : 125006.87383713324
},
"ok" : 1
}
这就完成了我们现在对地理空间信息的介绍;然而,在本书接下来的章节中,你会看到更多的例子来展示如何利用地理空间功能。
在现实世界中使用 MongoDB
现在您已经安装了 MongoDB 及其相关插件,并且已经了解了数据模型,是时候开始工作了。在本书接下来的五章中,您将学习如何构建、查询和操作各种示例 MongoDB 数据库(参见表 3-1 以快速浏览即将到来的主题)。每章将主要坚持使用该章独有的单一数据库;我们采用这种方法是为了以模块化的方式更容易阅读这本书。
表 3-1。
MongoDB Sample Databases Covered in This Book
| 回 | 数据库名称 | 主题 | | --- | --- | --- | | four | `Library` | 使用数据和索引 | | five | `Test` | 文件系统 | | six | `Contacts` | PHP 和 MongoDB | | seven | `Inventory` | Python 和 MongoDB | | eight | `Test` | 高级查询 |摘要
在这一章中,我们看了数据库后台发生的事情。我们还更深入地探讨了集合和文档的基本概念;我们还讨论了 MongoDB 中支持的数据类型,以及如何嵌入和引用数据。
接下来,我们研究了索引的作用,包括何时以及为什么应该使用(或不使用)索引。
我们还谈到了地理空间索引的概念。例如,我们讨论了如何存储地理空间数据;我们还解释了如何使用常规的find()函数或更基于地理空间的geoNear数据库命令来搜索这样的数据。
在下一章中,我们将进一步了解 MongoDB shell 是如何工作的,包括哪些函数可以用来插入、查找、更新或删除数据。我们还将探索条件运算符如何帮助您实现所有这些功能。
四、使用数据
Abstract
在前一章中,您学习了数据库在后端如何工作,什么是索引,如何使用数据库快速找到您正在寻找的数据,以及文档的结构是什么样的。您还看到了一个简单的例子,说明了如何添加数据并使用 MongoDB shell 再次找到数据。在这一章中,我们将更多地关注如何使用 shell 中的数据。
在前一章中,您学习了数据库在后端如何工作,什么是索引,如何使用数据库快速找到您正在寻找的数据,以及文档的结构是什么样的。您还看到了一个简单的例子,说明了如何添加数据并使用 MongoDB shell 再次找到数据。在这一章中,我们将更多地关注如何使用 shell 中的数据。
在本章中,我们将使用一个数据库(名为library),我们将执行诸如添加数据、搜索数据、修改数据、删除数据和创建索引之类的操作。我们还将了解如何使用各种命令导航数据库,以及 DBRef 是什么和它做什么。如果您已经按照前面章节中的说明设置了 MongoDB 软件,那么您可以按照本章中的示例来习惯这个界面。在这个过程中,您还将对哪些命令可以用于何种操作有一个坚实的理解。
浏览您的数据库
你需要知道的第一件事是如何浏览你的数据库和收藏。对于传统的 SQL 数据库,您需要做的第一件事是创建一个实际的数据库;但是,您可能还记得前面的章节,MongoDB 并不需要这样做,因为当您在其中存储数据时,程序会自动为您创建数据库和底层集合。
要切换到一个现有的数据库或创建一个新的数据库,您可以在 shell 中使用use函数,后跟您想要使用的数据库的名称,无论它是否存在。这个代码片段展示了如何使用library数据库:
> use library
Switched to db library
仅仅是调用use函数,然后调用数据库的名称,就可以将您的db(数据库)全局变量设置为library。这样做意味着您传递到 shell 中的所有命令将自动假定它们需要在library数据库上执行,直到您将这个变量重置到另一个数据库。
查看可用的数据库和集合
MongoDB 自动假设在您将数据保存到数据库时需要创建数据库。它也区分大小写。由于这些原因,确保您在正确的数据库中工作是相当棘手的。因此,在切换到一个数据库之前,最好先查看一下 MongoDB 当前可用的所有数据库的列表,以防忘记数据库的名称或确切的拼写。您可以使用show dbs功能来完成此操作:
> show dbs
admin
local
注意,这个函数只显示已经存在的数据库。在这个阶段,数据库还不包含任何数据,所以不会列出任何其他内容。如果您想查看当前数据库的所有可用集合,您可以使用show collections功能:
> show collections
system.indexes
请注意,system.indexes集合是在保存数据时自动创建的。此集合包含一个基于刚刚插入的文档中的_id key值的索引;它还包括您定义的任何自定义创建的索引。
Tip
要查看您当前正在使用的数据库,只需在 MongoDB shell 中键入db。
将数据插入集合
您想了解的最常用的功能之一是如何将数据插入到集合中。所有数据都是以 BSON 格式存储的(这种格式既紧凑,扫描速度也相当快),所以您还需要插入 BSON 格式的数据。你可以用几种方法做到这一点。例如,您可以首先定义它,然后使用insert函数将其保存在集合中,或者您可以在运行时使用insert函数键入文档:
> document = ( { "Type" : "Book", "Title" : "Definitive Guide to MongoDB 2nd ed.,
The", "ISBN" : "978-1-4302-5821-6", "Publisher" : "Apress", "Author": [
"Hows, David", "Plugge, Eelco", "Membrey, Peter", "Hawkins, Tim" ] } )
Note
当你在 shell 中定义一个变量时(例如document = ( { ... } ),变量的内容会被立即打印出来。
> db.media.insert(document)
在 shell 中键入时也可以使用换行符。如果您正在编写一个相当长的文档,这可能很方便,如下例所示:
> document = ( { "Type" : "Book",
..."Title" : "Definitive Guide to MongoDB 2nd ed., The",
..."ISBN" : "978-1-4302-5821-6",
..."Publisher" : "Apress",
..."Author" : ["Hows, David", Plugge, Eelco", "Membrey, Peter"," "Hawkins, Tim"]
...} )
> db.media.insert(document)
如上所述,另一种选择是直接通过 shell 插入数据,而不需要首先定义文档。您可以通过立即调用insert函数,然后调用文档的内容来做到这一点:
> db.media.insert( { "Type" : "CD", "Artist" : "Nirvana", "Title" : "Nevermind" })
或者您可以像以前一样,在使用换行符的同时插入数据。例如,您可以通过添加轨迹数组来扩展前面的示例。请密切注意以下示例中逗号和括号的用法:
> db.media.insert( { "Type" : "CD",
..."Artist" : "Nirvana",
..."Title" : "Nevermind",
... "Tracklist" : [
... {
... "Track" : "1",
... "Title" : "Smells Like Teen Spirit",
... "Length" : "5:02"
... },
... {
... "Track" : "2",
... "Title" : "In Bloom",
... "Length" : "4:15"
... }
... ]
...}
... )
如您所见,通过 Mongo shell 插入数据非常简单。
插入数据的过程非常灵活,但是在这样做的时候,您必须遵守一些规则。例如,插入文档时键的名称有以下限制:
$字符不能是键名中的第一个字符。示例:$tags- 句点[
.]字符不得出现在键名中的任何位置。示例:ta.gs - 名称
_id保留用作主键 ID;虽然不建议这样做,但它可以将任何唯一的东西存储为值,如字符串或整数。
同样,创建集合时也有一些限制。例如,集合的名称必须遵循以下规则:
- 集合名称不能超过 128 个字符。
- 空字符串(" ")不能用作集合名称。
- 集合名称必须以字母或下划线开头。
- 集合名
system是为 MongoDB 保留的,不能使用。 - 集合名称不能包含“\0”空字符。
查询数据
您已经看到了如何切换到您的数据库以及如何插入数据;接下来,您将学习如何查询集合中的数据。让我们在前一个例子的基础上,研究所有可能的方法,以便清楚地查看给定集合中的数据。
Note
当查询数据时,您有大量的选项、操作符、表达式、过滤器等可供选择。我们将在接下来的几节中回顾这些选项。
find()函数提供了从一个集合中的多个文档中检索数据的最简单方法。该功能是您将经常使用的功能之一。
让我们假设您已经将前面的两个例子插入到了数据库library中名为media的集合中。如果您要在这个集合上使用一个简单的find()函数,您将会得到到目前为止您添加的所有文档:
> db.media.find()
{ "_id" : "ObjectId("4c1a8a56c603000000007ecb"), "Type" : "Book", "Title" :
"Definitive Guide to MongoDB 2nd ed., The", "ISBN" : "978-1-4302-5821-6", "Publisher" :
"Apress", "Author" : ["Hows, David ", "Plugge, Eelco", "Membrey, Peter", "Hawkins, Tim"]}
{ "_id" : "ObjectId("4c1a86bb2955000000004076"), "Type" : "CD", "Artist" :
"Nirvana", "Title" : "Nevermind", "Tracklist" : [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Length" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
] }
这很简单,但是通常您不希望从集合中的所有文档中检索所有信息。相反,您可能希望检索某种类型的文档。例如,您可能想要归还 Nirvana 的所有 CD。如果是这样,您可以指定只请求和返回所需的信息:
> db.media.find ( { Artist : "Nirvana" } )
{ "_id" : "ObjectId("4c1a86bb2955000000004076"), "Type" : "CD", "Artist" :
"Nirvana", "Title" : "Nevermind", "Tracklist" : [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Length" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
] }
好吧,这样看起来好多了!您不必查看已添加到收藏中的所有其他项目的所有信息,只需查看您感兴趣的信息。但是,如果您仍然对返回的结果不满意,该怎么办呢?例如,假设您想要获取一个列表,该列表只显示您拥有的 Nirvana CD 的标题,而忽略任何其他信息,比如曲目列表。您可以通过在查询中插入一个附加参数来实现这一点,该参数指定您想要返回的键的名称,后跟一个1:
> db.media.find ( {Artist : "Nirvana"}, {Title: 1} )
{ "_id" : ObjectId("4c1a86bb2955000000004076"), "Title" : "Nevermind" }
插入{ Title : 1 }信息指定只返回来自标题字段的信息。结果被排序并以升序呈现给你。
Note
升序基于文档的插入顺序。
您也可以完成相反的操作:插入{ Type : 0 }检索您从 Nirvana 存储的所有项目的列表,显示除了Type字段之外的所有信息。
Note
默认情况下,_id字段将保持可见,除非您明确要求它不要显示自己。
花点时间运行插入了{ Title : 1 }的修改后的查询;根本不会返回任何不必要的信息。这将节省您的时间,因为您只看到您想要的信息。它还节省了数据库返回不必要信息所需的时间。
使用点符号
当您开始处理更复杂的文档结构(如包含数组或嵌入对象的文档)时,您也可以开始使用其他方法从这些对象中查询信息。例如,假设您想要查找包含您喜欢的特定歌曲的所有 CD。以下代码执行更详细的查询:
> db.media.find( { "Tracklist.Title" : "In Bloom" } )
{ "_id" : "ObjectId("4c1a86bb2955000000004076"), "Type" : "CD", "Artist" :
"Nirvana", "Title" : "Nevermind", "Tracklist" : [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Length" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
] }
在键名后使用句点[ . ]告诉您的find函数查找文档中嵌入的信息。使用数组时,事情要简单一些。例如,如果要查找 Peter Membrey 所写的书籍列表,可以执行以下查询:
> db.media.find( { "Author" : "Membrey, Peter" } )
{ "_id" : "ObjectId("4c1a8a56c603000000007ecb"), "Type" : "Book", "Title" :
"Definitive Guide to MongoDB 2nd ed., The", "ISBN" : "978-1-4302-5821-6", "Publisher" :
"Apress", "Author" : ["Hows, David ", "Plugge, Eelco", "Membrey, Peter", "Hawkins, Tim"] }
但是,以下命令不会匹配任何文档,即使它可能看起来与前面的 track list 查询相同:
> db.media.find ( { "Tracklist" : {"Track" : "1" }} )
子对象必须完全匹配;因此,前面的查询只匹配不包含其他信息的文档,比如Track.Title:
{"Type" : "CD",
"Artist" : "Nirvana"
"Title" : "Nevermind",
"Tracklist" : [
{
"Track" : "1",
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
]
}
使用排序、限制和跳过功能
MongoDB 包括几个函数,您可以使用它们来更精确地控制您的查询。我们将在本节讲述如何使用sort、limit和skip功能。
您可以使用sort函数对查询返回的结果进行排序。您可以分别使用1或-1对结果进行升序或降序排序。该函数本身类似于 SQL 中的ORDER BY语句,它使用键的名称和排序方法作为标准,如下例所示:
> db.media.find().sort( { Title: 1 })
这个例子根据Title键值对结果进行升序排序。当没有指定参数时,这是默认的排序顺序。您可以添加-1标志来按降序排序。
Note
如果指定了一个不存在的排序键,则值将按升序插入顺序返回。
您可以使用limit()函数来指定返回结果的最大数量。这个函数只需要一个参数:返回的期望结果的数量。当您指定“0”时,将返回所有结果。以下示例仅返回媒体集合中的前十个项目:
> db.media.find().limit( 10 )
您可能想做的另一件事是跳过集合中的前 n 个文档。以下示例跳过媒体集合中的前二十个文档:
> db.media.find().skip( 20 )
正如您可能猜测的那样,这个命令返回集合中的所有文档,除了它找到的前二十个。记住:它按照文档插入的顺序查找文档。
如果 MongoDB 不能组合这些命令,它就不会特别强大。然而,实际上任何功能都可以与任何其他功能结合使用。以下示例通过跳过一些结果来限制结果,然后按降序对结果进行排序:
> db.media.find().sort ( { Title : -1 } ).limit ( 10 ).skip ( 20 )
如果希望在应用中实现分页,可以使用这个示例。正如您可能已经猜到的那样,该命令不会返回到目前为止创建的媒体集合中的任何结果,因为该集合包含的文档比本例中跳过的要少。
Note
您可以在find()功能中使用以下快捷方式来跳过和限制您的结果:find ( {}, {}, 10, 20 )。这里,您将结果限制为 10 个,并跳过前 20 个文档。
使用封顶集合、自然顺序和$natural
在使用 MongoDB 对查询进行排序时,您应该了解一些额外的概念和特性,包括上限集合、自然顺序和$natural。我们将在本节解释所有这些术语的含义,以及如何在您的分类中利用它们。
自然顺序是数据库对(正常)集合中的对象的本机排序方法。因此,当您查询集合中的项时,默认情况下,这些项以正向自然顺序返回。这通常与插入项目的顺序相同;但是,这并不能保证,因为当数据被修改后不再适合原来的位置时,它可能会移动。
capped 集合是数据库中的一个集合,其中自然顺序保证是文档插入的顺序。当您查询数据并且需要绝对确定返回的结果已经根据插入顺序进行了排序时,保证自然顺序始终与插入顺序相匹配特别有用。
有上限的集合还有另一个好处:它们的大小是固定的。一旦封顶的集合已满,最旧的数据将被清除,较新的数据将被添加到末尾,从而确保自然顺序遵循记录的插入顺序。这种类型的收集可用于记录和自动归档数据。
与标准集合不同,封顶集合必须使用createCollection函数显式创建。您还必须提供指定要添加的集合的大小(以字节为单位)的参数。例如,假设您想要创建一个名为audit的上限集合,最大大小为 20480 字节:
> db.createCollection("audit", {capped:true, size:20480})
{ "ok" : 1 }
假设有上限的集合保证了自然顺序与插入顺序相匹配,那么在查询数据时,您也不需要包含任何特殊的参数或任何其他特殊的命令或函数,当然,当您想要反转默认结果时除外。这就是$natural参数的用武之地。例如,假设您想从列出失败登录尝试的 capped 集合中找到最近的 10 个条目。您可以使用$natural参数来查找这些信息:
> db.audit.find().sort( { $natural: -1 } ).limit ( 10 )
Note
已经添加到 capped 集合的文档可以更新,但是它们的大小不能增加。如果这样,更新将会失败。从封顶的集合中删除文档也是不可能的;相反,如果要这样做,必须删除并重新创建整个集合。在本章的后面,您将了解更多关于删除收藏的信息。
您还可以在创建集合时使用max:参数来限制添加到 capped 集合中的项目数量。但是,必须注意确保集合中有足够的空间来容纳要添加的项目数。如果在达到项目数之前集合已满,集合中最旧的项目将被移除。MongoDB shell 包含一个实用程序,可以让您查看现有集合使用的空间量,无论它是否有上限。您可以使用validate()函数调用这个实用程序。如果您想估计集合可能会变得有多大,这可能特别有用。
如前所述,您可以使用max:参数来限制可以插入到集合中的项目数量,如下例所示:
> db.createCollection("audit100", { capped:true, size:20480, max: 100})
{ "ok" : 1 }
接下来,使用validate()函数检查集合的大小:
> db.audit100.validate()
{
"ns" : "media.audit100",
"result" : "
validate
capped:1 max:100
firstExtent:0:54000 ns:media.audit100
lastExtent:0:54000 ns:media.audit100
# extents:1
datasize?:0 nrecords?:0 lastExtentSize:20736
padding:1
first extent:
loc:0:54000 xnext:null xprev:null
nsdiag:media.audit100
size:20736 firstRecord:null lastRecord:null
capped outOfOrder:0 (OK)
0 objects found, nobj:0
0 bytes data w/headers
0 bytes data wout/headers
deletedList: 1100000000000000000
deleted: n: 2 size: 20560
nIndexes:0
",
"ok" : 1,
"valid" : true,
"lastExtentSize" : 20736
}
结果输出显示,表(名为audit100)是一个上限集合,最多可以添加 100 个条目,目前它不包含任何条目。
检索单个文档
到目前为止,我们只看了展示如何检索多个文档的例子。然而,如果您只想接收一个结果,查询所有文档——这是您在执行find()函数时通常会做的——将会浪费 CPU 时间和内存。对于这种情况,您可以使用findOne()函数从您的集合中检索单个项目。总的来说,结果和添加limit(1)函数时的结果是一样的,但是为什么要给自己增加不必要的困难呢?
findOne()函数的语法与find()函数的语法相同:
> db.media.findOne()
如果你只期望一个结果,通常建议使用findOne()函数。
使用聚合命令
MongoDB 附带了一组很好的聚合命令。一开始您可能看不到它们的重要性,但是一旦您掌握了它们,您将会看到聚合命令形成了一套极其强大的工具。例如,您可以使用它们来获得关于数据库的一些基本统计信息的概述。在这一节中,我们将仔细研究如何使用可用的聚合命令中的三个函数:count、distinct和group。
除了这三个基本的聚合命令,MongoDB 还包括一个聚合框架。这个强大的特性将允许您计算聚合值,而无需使用通常过于复杂的 map/reduce 框架。聚合框架将在第 5 章中讨论。
使用 count()返回文档数
函数的作用是:返回指定集合中文档的数量。到目前为止,我们已经在媒体集合中添加了许多文档。count()函数可以告诉您确切的数量:
> db.media.count()
Two
您还可以通过将count()与条件操作符结合起来执行额外的过滤,如下所示:
> db.media.find( { Publisher : "Apress", Type: "Book" } ).count()
1
本示例仅返回添加到集合中的由出版社出版且类型为 Book 的文档数。注意,count()功能默认忽略一个skip()或limit()参数。为了确保您的查询不会跳过这些参数,并且您的计数结果将匹配limit和/或skip参数,请使用count(true):
> db.media.find( { Publisher: "Apress", Type: "Book" }).skip ( 2 ) .count (true)
0
使用 distinct()检索唯一值
前面的示例展示了一种从特定发布者处检索文档总数的好方法。然而,这种方法绝对不精确。毕竟,如果你拥有多本同名的书(例如,纸质书和电子书),那么从技术上讲,你就只有一本书。这就是distinct()可以帮助你的地方:它只会返回唯一的值。
为了完整起见,您可以向集合中添加一个额外的项目。这个项目有相同的标题,但有不同的 ISBN 号:
> document = ( { "Type" : "Book","Title" : "Definitive Guide to MongoDB 2nd ed., The", ISBN:
"978-1-4302-5821-6", "Publisher" : "Apress", "Author" :
["Hows, David","Membrey, Peter","Plugge, Eelco","Hawkins, Tim"] } )
> db.media.insert (document)
此时,数据库中应该有两本书名相同的书。对该系列中的标题使用distinct()功能时,您将总共获得两个独特的项目。但两本书的书名都是独一无二的,所以会归为一项。另一个结果将是专辑的名字“没关系:”
> db.media.distinct( "Title")
[ "Definitive Guide to MongoDB, The", "Nevermind" ]
类似地,如果您查询唯一的 ISBN 号列表,您将得到两个结果:
> db.media.distinct ("ISBN")
[ "1-4302-3051-7", "987-4302-3051-9" ]
distinct()函数在查询时也采用嵌套键;例如,该命令将为您提供 CD 的唯一标题列表:
> db.media.distinct ("Tracklist.Title")
[ "In Bloom", "Smells Like Teen Spirit" ]
将您的结果分组
最后但同样重要的是,你可以将你的结果分组。MongoDB 的group()函数类似于 SQL 的GROUP BY函数,虽然语法有点不同。该命令的目的是返回分组项目的数组。group()函数有三个参数:key、initial和reduce。
key参数指定您想要分组的结果。例如,假设您想按Title对结果进行分组。initial参数允许您为每个分组结果提供一个基数(也就是开始时项目的基数)。默认情况下,如果希望返回一个精确的数字,可以将该参数设置为零。reduce参数将所有相似的项目组合在一起。Reduce 有两个参数:正在迭代的当前文档和聚合计数器对象。在下面的例子中,这些参数被称为items和prev。从本质上来说,reduce参数会将一个1加到它遇到的每个与它已经找到的标题相匹配的条目的总和上。
当您在寻找一个tagcloud类型的函数时,group()函数是理想的。例如,假设您想要获取收藏中任何类型项目的所有唯一标题的列表。此外,假设您希望根据标题将它们分组在一起(如果找到了任何重复项):
> db.media.group (
{
key: {Title : true},
initial: {Total : 0},
reduce : function (items,prev)
{
prev.Total += 13
}
}
)
[
{
"Title" : "Nevermind",
"Total" : 1
},
{
"Title" : "Definitive Guide to MongoDB, The",
"Total" : 2
}
]
除了key、initial和reduce参数外,您还可以指定三个可选参数:
keyf:如果您不希望根据文档中的现有关键字对结果进行分组,您可以使用此参数替换key参数。相反,您可以使用您设计的另一个指定如何进行分组的函数对它们进行分组。cond:您可以使用此参数来指定一个附加语句,该语句在文档被分组之前必须为真。您可以像使用find()查询在您的集合中搜索文档一样使用它。如果未设置该参数(默认),则将检查集合中的所有文档。finalize:您可以使用该参数来指定在最终结果返回之前您想要执行的功能。例如,您可以计算平均值或执行计数,并将此信息包含在结果中。
Note
group()函数目前在分片环境中不工作。对于这些,你应该使用mapreduce()函数。此外,使用group()函数得到的输出不能包含超过 10,000 个键,否则将引发异常。这也可以通过使用mapreduce()来绕过。
使用条件运算符
MongoDB 支持大量的条件操作符来更好地过滤您的结果。以下部分提供了这些运算符的概述,包括一些向您展示如何使用它们的基本示例。然而,在浏览这些示例之前,您应该向数据库中添加一些条目;这样做会让您更清楚地看到这些运算符的效果:
dvd = ( { "Type" : "DVD", "Title" : "Matrix, The", "Released" : 1999,
"Cast" : ["Keanu Reeves","Carrie-Anne Moss","Laurence Fishburne","Hugo
Weaving","Gloria Foster","Joe Pantoliano"] } )
{
"Type" : "DVD",
"Title" : "Matrix, The",
"Released" : 1999,
"Cast" : [
"Keanu Reeves",
"Carrie-Anne Moss",
"Laurence Fishburne",
"Hugo Weaving",
"Gloria Foster",
"Joe Pantoliano"
]
}
> db.media.insert(dvd)
> dvd = ( { "Type" : "DVD", Title : "Blade Runner", Released : 1982 } )
{ "Type" : "DVD", "Title" : "Blade Runner", "Released" : 1982 }
> db.media.insert(dvd)
> dvd = ( { "Type" : "DVD", Title : "Toy Story 3", Released : 2010 } )
{ "Type" : "DVD", "Title" : "Toy Story 3", "Released" : 2010 }
> db.media.insert(dvd)
执行大于和小于比较
您可以使用以下特殊参数在查询中执行大于和小于比较:$gt、$lt、$gte和$lte。在这一节中,我们将看看如何使用这些参数。
我们将涉及的第一个是$gt(大于)参数。您可以使用它来指定某个整数应该大于指定的值才能被返回:
> db.media.find ( { Released : {$gt : 2000} }, { "Cast" : 0 } )
{ "_id" : ObjectId("4c4369a3c603000000007ed3"), "Type" : "DVD", "Title" :
"Toy Story 3", "Released" : 2010 }
请注意,2000 年本身将不包括在前面的查询中。为此,您可以使用$gte(大于或等于)参数:
> db.media.find ( { Released : {$gte : 1999 } }, { "Cast" : 0 } )
{ "_id" : ObjectId("4c43694bc603000000007ed1"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999 }
{ "_id" : ObjectId("4c4369a3c603000000007ed3"), "Type" : "DVD", "Title" :
"Toy Story 3", "Released" : 2010 }
同样,您可以使用$lt(小于)参数来查找您的集合中早于 1999 年的项目:
> db.media.find ( { Released : {$lt : 1999 } }, { "Cast" : 0 } )
{ "_id" : ObjectId("4c436969c603000000007ed2"), "Type" : "DVD", "Title" : "Blade Runner", "Released" : 1982 }
您还可以通过使用$lte(小于或等于)参数获得早于或等于 1999 年的项目列表:
> db.media.find( {Released : {$lte: 1999}}, { "Cast" : 0 })
{ "_id" : ObjectId("4c43694bc603000000007ed1"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999 }
{ "_id" : ObjectId("4c436969c603000000007ed2"), "Type" : "DVD", "Title" :
"Blade Runner", "Released" : 1982 }
您也可以组合这些参数来指定范围:
> db.media.find( {Released : {$gte: 1990, $lt : 2010}}, { "Cast" : 0 })
{ "_id" : ObjectId("4c43694bc603000000007ed1"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999 }
您可能会觉得这些参数使用起来相对简单;但是,在查询特定范围的数据时,您会经常用到它们。
检索除指定文档之外的所有文档
您可以使用$ne (not equals)参数来检索集合中的每个文档,除了那些符合特定标准的文档。例如,您可以使用这个代码片段来获取作者不是 Eelco Plugge 的所有书籍的列表:
> db.media.find( { Type : "Book", Author: {$ne : "Plugge, Eelco"}})
指定匹配的数组
您可以使用$in操作符来指定一个可能匹配的数组。SQL 的等价物是IN操作符。
您可以使用下面的代码片段通过使用$in操作符从媒体集合中检索数据:
> db.media.find( {Released : {$in : [1999,2008,2009] } }, { "Cast" : 0 } )
{ "_id" : ObjectId("4c43694bc603000000007ed1"), "Type" : "DVD", "Title" : "Matrix, The", "Released" : 1999 }
此示例只返回一个项目,因为只有一个项目与 1999 年的发布年份匹配,而 2008 年和 2009 年没有匹配项。
查找不在数组中的值
$nin操作符的功能类似于$in操作符,除了它搜索指定字段在指定数组中没有值的对象:
> db.media.find( {Released : {$nin : [1999,2008,2009] },Type : "DVD" },
{ "Cast" : 0 } )
{ "_id" : ObjectId("4c436969c603000000007ed2"), "Type" : "DVD", "Title" :
"Blade Runner", "Released" : 1982 }
{ "_id" : ObjectId("4c4369a3c603000000007ed3"), "Type" : "DVD", "Title" :
"Toy Story 3", "Released" : 2010 }
匹配文档中的所有属性
$all操作符的工作方式与$in操作符类似。然而,$all要求文档中的所有属性都匹配,而对于$in操作符,只有一个属性必须匹配。让我们看一个例子来说明这些差异。首先,这里有一个使用$in的例子:
> db.media.find ( { Released : {$in : ["2010","2009"] } }, { "Cast" : 0 } )
{ "_id" : ObjectId("4c4369a3c603000000007ed3"), "Type" : "DVD", "Title" :
"Toy Story 3", "Released" : 2010 }
为$in操作符返回一个文档,因为有 2010 年的匹配,但没有 2009 年的匹配。但是,$all参数不返回任何结果,因为没有值为 2009 的匹配文档:
> db.media.find ( { Released : {$all : ["2010","2009"] } }, { "Cast" : 0 } )
在文档中搜索多个表达式
您可以使用$or操作符在单个查询中搜索多个表达式,其中只需要匹配一个标准来返回给定的文档。与$in操作符不同,$or允许您指定键和值,而不仅仅是值:
> db.media.find({ $or : [ { "Title" : "Toy Story 3" }, { "ISBN" :
"987-1-4302-3051-9" } ] } )
{ "_id" : ObjectId("4c5fc7d8db290000000067c5"), "Type" : "Book", "Title" :
"Definitive Guide to MongoDB, The", "ISBN" : "987-1-4302-3051-9",
"Publisher" : "Apress", "Author" : ["Hows, David", "Membrey, Peter", "Plugge, Eelco",
"Hawkins, Tim" ] }
{ "_id" : ObjectId("4c5fc943db290000000067ca"), "Type" : "DVD", "Title" :
"Toy Story 3", "Released" : 2010 }
还可以将$or操作符与另一个查询参数结合起来。这将把返回的文档限制为只与第一个查询匹配的文档(强制),然后是在$or操作符中指定的两个键/值对中的一个,如下例所示:
> db.media.find({ "Type" : "DVD", $or : [ { "Title" : "Toy Story 3" }, {
"ISBN" : "987-1-4302-3051-9" } ] })
{ "_id" : ObjectId("4c5fc943db290000000067ca"), "Type" : "DVD", "Title" :
"Toy Story 3", "Released" : 2010 }
您可以说,$or操作符允许您同时执行两个查询,将两个原本不相关的查询的结果组合起来。
使用$slice 检索文档
您可以使用$ slice操作符从文档的数组中检索包含特定区域的文档。如果您想要限制添加的某一组项目以节省带宽,这可能特别有用。操作符还允许您在每页上检索 n 个结果,这个特性通常被称为分页。
理论上,$slice操作符结合了limit()和skip()函数的功能;然而,limit()和skip()不能在数组上工作,而$slice可以。操作符有两个参数:第一个指示要返回的项目总数。第二个参数是可选的;如果使用,它确保第一个参数定义偏移,而第二个参数定义限制。极限参数也可以指示负条件。
下面的示例将强制转换列表中的项目限制为前三项:
> db.media.find({"Title" : "Matrix, The"}, {"Cast" : {$slice: 3}})
{ "_id" : ObjectId("4c5fcd3edb290000000067cb"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999, "Cast" : [ "Keanu Reeves", "Carrie-Anne
Moss", "Laurence Fishburne" ] }
通过使整数为负,也可以只获得最后三项:
> db.media.find({"Title" : "Matrix, The"}, {"Cast" : {$slice: -3}})
{ "_id" : ObjectId("4c5fcd3edb290000000067cb"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999, "Cast" : [ "Hugo Weaving", "Gloria Foster",
"Joe Pantoliano" ] }
或者,您可以跳过前两项,从该特定点开始将结果限制为三项(注意括号):
> db.media.find({"Title" : "Matrix, The"}, {"Cast" : {$slice: [2,3] }})
{ "_id" : ObjectId("4c5fcd3edb290000000067cb"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999, "Cast" : [ "Laurence Fishburne", "Hugo
Weaving", "Gloria Foster" ] }
最后,当指定负整数时,可以跳到最后五项,并将结果限制为四项,如下例所示:
> db.media.find({"Title" : "Matrix, The"}, {"Cast" : {$slice: [-5,4] }})
{ "_id" : ObjectId("4c5fcd3edb290000000067cb"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999, "Cast" : [ "Carrie-Anne Moss","Laurence
Fishburne","Hugo Weaving","Gloria Foster"] }
Note
在 2.4 版本中,MongoDB 还为$push操作引入了$slice操作符,允许您在向数组追加值时限制数组元素的数量。本章稍后将讨论该运算符。但是,不要混淆这两者。
搜索奇/偶整数
$mod运算符允许您搜索由偶数或奇数组成的特定数据。这是可行的,因为操作符取2的模数并检查0的余数,从而只提供偶数的结果。
例如,以下代码返回集合中的任何项目,该项目的Released字段设置为偶数整数:
> db.media.find ( { Released : { $mod: [2,0] } }, {"Cast" : 0 } )
{ "_id" : ObjectId("4c45b5c18e0f0000000062aa"), "Type" : "DVD", "Title" :
"Blade Runner", "Released" : 1982 }
{ "_id" : ObjectId("4c45b5df8e0f0000000062ab"), "Type" : "DVD", "Title" :
"Toy Story 3", "Released" : 2010 }
同样,您可以通过更改$mod中的参数来查找任何在Released字段中包含不均匀值的文档,如下所示:
> db.media.find ( { Released : { $mod: [2,1] } }, { "Cast" : 0 } )
{ "_id" : ObjectId("4c45b5b38e0f0000000062a9"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999 }
Note
$mod操作符只对整数值有效,对包含数字值的字符串无效。例如,您不能在{ Released : "2010" }上使用操作符,因为它在引号中,因此是一个字符串。
使用$size 过滤结果
$size操作符允许您过滤结果,以匹配一个包含指定数量元素的数组。例如,您可以使用此运算符来搜索那些恰好包含两首歌曲的 CD:
> db.media.find ( { Tracklist : {$size : 2} } )
{ "_id" : ObjectId("4c1a86bb2955000000004076"), "Type" : "CD", "Artist" :
"Nirvana", "Title" : "Nevermind", "Tracklist" : [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Lenght" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
] }
Note
您不能使用$size运算符来查找尺寸范围。例如,不能用它来查找包含多个元素的数组。
返回特定的字段对象
$exists操作符允许您在指定的字段丢失或找到时返回一个特定的对象。以下示例返回集合中具有名为Author的关键字的所有项目:
> db.media.find ( { Author : {$exists : true } } )
类似地,如果用值false调用这个操作符,那么将返回所有没有名为Author的键的文档:
> db.media.find ( { Author : {$exists : false } } )
Warning
目前,$exists操作符不能使用索引;因此,使用它需要全表扫描。
基于 BSON 类型的匹配结果
$type操作符允许您根据 BSON 类型匹配结果。例如,下面的代码片段让您找到所有具有类型为Embedded Object的跟踪列表的项目(也就是说,它包含一个信息列表):
> db.media.find ( { Tracklist: { $type : 3 } } )
{ "_id" : ObjectId("4c1a86bb2955000000004076"), "Type" : "CD", "Artist" :
"Nirvana", "Title" : "Nevermind", "Tracklist" : [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Lenght" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
] }
表 4-1 中定义了已知的数据类型。
表 4-1。
Known BSON Types and Codes
| 密码 | 数据类型 | | --- | --- | | –1 | 迷你键 | | one | 两倍 | | Two | 字符串(UTF8) | | three | 嵌入对象 | | four | 嵌入式阵列 | | five | 二进制数据 | | 七 | 对象 ID | | eight | 布尔型 | | nine | 日期类型 | | Ten | 零型 | | Eleven | 正则表达式 | | Thirteen | JavaScript 代码 | | Fourteen | 标志 | | Fifteen | 带作用域的 JavaScript 代码 | | Sixteen | 32 位整数 | | Seventeen | 时间戳 | | Eighteen | 64 位整数 | | One hundred and twenty-seven | MaxKey | | Two hundred and fifty-five | 最小键 |匹配整个数组
如果想匹配文档中的整个数组,可以使用$elemMatch操作符。如果您的集合中有多个文档,其中一些文档包含一些相同的信息,这将非常有用。这可能会使默认查询无法找到您正在寻找的确切文档。这是因为标准查询语法并不局限于数组中的单个文档。
让我们看一个例子来说明这个原则。要做到这一点,我们需要向集合中添加另一个文档,这个文档中有一个相同的条目,但在其他方面有所不同。具体来说,我们将添加 Nirvana 的另一张 CD,这张 CD 恰好与前面提到的 CD 有相同的曲目(“闻起来像青少年精神”)。然而,在这个版本的 CD 上,歌曲是音轨 5,而不是音轨 1:
{
"Type" : "CD",
"Artist" : "Nirvana",
"Title" : "Nirvana",
"Tracklist" : [
{
"Track" : "1",
"Title" : "You know you're right",
"Length" : "3:38"
},
{
"Track" : "5",
"Title" : "Smells like teen spirit",
"Length" : "5:02"
}
]
}
> nirvana = ( { "Type" : "CD", "Artist" : "Nirvana", "Title" : "Nirvana",
"Tracklist" : [ { "Track" : "1", "Title" : "You Know You're Right", "Length"
: "3:38"}, {"Track" : "5", "Title" : "Smells Like Teen Spirit", "Length" :
"5:02" } ] } )
> db.media.insert(nirvana)
如果您想在 CD 上搜索歌曲“smokes Like Teen Spirit”作为曲目 1 的 Nirvana 专辑,您可能会认为下面的查询可以满足您的要求:
> db.media.find ( { "Tracklist.Title" : "Smells Like Teen Spirit",
"Tracklist.Track" : "1" } )
不幸的是,前面的查询将返回这两个文档。这样做的原因是两个文档都有一个标题为“smokes Like Teen Spirit”的音轨,并且都有音轨编号 1。如果想要匹配数组中的整个文档,可以使用$elemMatch,如下例所示:
> db.media.find ( { Tracklist: { "$elemMatch" : { Title:
"Smells like teen spirit", Track : "1" } } } )
{ "_id" : ObjectId("4c1a86bb2955000000004076"), "Type" : "CD", "Artist" :
"Nirvana", "Title" : "Nevermind", "Tracklist" : [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Lenght" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
] }
这个查询给出了期望的结果,并且只返回了第一个文档。
$not(元运算符)
您可以使用$not元操作符来否定标准操作符执行的任何检查。以下示例返回集合中的所有文档,除了在$elemMatch示例中看到的文档:
> db.media.find ( { Tracklist : { $not : { "$elemMatch" : { Title:
"Smells Like Teen Spirit", "Track" : "1" } } } } )
指定附加查询表达式
除了到目前为止您已经看到的结构化查询语法之外,您还可以在 JavaScript 中指定附加的查询表达式。这样做的最大好处是 JavaScript 非常灵活,允许您做大量额外的事情。使用 JavaScript 的缺点是比 MongoDB 内置的原生操作符慢一点。
例如,假设您想在您的收藏中搜索一张 1995 年以前的 DVD。以下所有代码示例都将返回此信息:
db.media.find ( { "Type" : "DVD", "Released" : { $lt : 1995 } } )
db.media.find ( { "Type" : "DVD", $where: "this.Released < 1995" } )
db.media.find ("this.Released < 1995")
f = function() { return this.Released < 1995 }
db.media.find(f)
这就是 MongoDB 的灵活性!使用这些操作符应该能够让您在您的收藏中找到任何东西。
利用正则表达式
正则表达式是另一个可以用来查询信息的强大工具。正则表达式(简称 regex)是特殊的文本字符串,可以用来描述搜索模式。这些功能很像通配符,但是它们更强大、更灵活。
MongoDB 允许您在集合中搜索数据时使用这些正则表达式;但是,对于简单的前缀查询,它将尽可能使用索引。
以下示例在查询中使用 regex 来查找媒体集合中以单词“Matrix”开头的所有项目
> db.media.find ( { Title : /Matrix*/i } )
使用 MongoDB 的正则表达式可以让您的生活变得更加简单,所以我们建议在时间允许的情况下更详细地探索这个特性,或者您的环境可以从中受益。
更新数据
到目前为止,您已经学习了如何在数据库中插入和查询数据。接下来,您将学习如何更新这些数据。MongoDB 支持相当多的更新操作符,您将在下面的小节中学习如何使用这些操作符。
使用 update()更新
MongoDB 附带了用于更新数据的update()函数。update()函数有三个主要参数:criteria、objNew和options。
criteria参数允许您指定选择想要更新的记录的查询。您使用objNew参数来指定更新的信息;或者你可以使用一个运营商来为你做这件事。options参数允许您在更新文档时指定选项,有两个可能的值:upsert和multi。upsert选项让您指定更新是否应该是 up sert——也就是说,它告诉 MongoDB 如果记录存在就更新它,如果不存在就创建它。最后,multi选项让您指定是应该更新所有匹配的文档还是只更新第一个文档(默认操作)。
下面这个简单的例子使用了update()函数,没有任何花哨的操作符:
> db.media.update( { "Title" : "Matrix, The"}, {"Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999, "Genre" : "Action"}, { upsert: true} )
本示例覆盖集合中的文档,并用指定的新值保存。请注意,您遗漏的任何字段都将被删除(文档基本上被重写)。因为upsert参数被指定为true,所以任何尚不存在的字段都将被添加进来(在本例中是Genre键/值对)。
如果碰巧有多个文档符合标准,并且您希望将它们全部向上插入,可以在使用$set修饰符操作符的同时添加upsert和multi选项,如下所示:
> db.media.update( { "Title" : "Matrix, The"}, {$set: {"Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999, "Genre" : "Action"} }, {upsert: true, multi: true} )
Note
一个upsert告诉数据库“如果一个文档存在就更新一个记录,如果不存在就插入记录。”
使用 save()命令实现 Upsert
您也可以使用save()命令执行向上插入。为此,您需要指定_id值;您可以自动添加该值,也可以自己手动指定。如果您没有指定_id值,save()命令将假设它是一个插入,并简单地将文档添加到您的集合中。
使用save()命令的主要好处是你不需要指定upsert方法应该和update()命令一起使用。因此,save()命令为您提供了一种更快捷的方式来更新数据。实际上,save()和update()命令看起来很相似:
> db.media.update( { "Title" : "Matrix, The"}, {"Type" : "DVD", "Title" :
"Matrix, The", "Released" : "1999", "Genre" : "Action"}, { upsert: true} )
> db.media.save( { "Title" : "Matrix, The"}, {"Type" : "DVD", "Title" :
"Matrix, The", "Released" : "1999", "Genre" : "Action"})
显然,这个例子假设Title值作为id字段。
自动更新信息
您可以使用修饰符操作快速、简单地更新文档中的信息,但不需要手动键入所有内容。例如,您可以使用这些操作来增加一个数字或从数组中移除一个元素。
接下来我们将探索这些操作符,提供实际的例子来展示如何使用它们。
用$inc 增加一个值
$inc操作符使您能够对一个键执行(原子)更新,以给定的增量增加值,假设该字段存在。如果该字段不存在,将会被创建。要查看这一过程,首先向集合中添加另一个文档:
> manga = ( { "Type" : "Manga", "Title" : "One Piece", "Volumes" : 612,
"Read" : 520 } )
{
"Type" : "Manga",
"Title" : "One Piece",
"Volumes" : "612",
"Read" : "520"
}
> db.media.insert(manga)
现在您已经准备好更新文档了。例如,假设您已经阅读了另外四卷《海贼王》漫画,并且您想要增加文档中的Read卷数。以下示例向您展示了如何做到这一点:
> db.media.update ( { "Title" : "One Piece"}, {$inc: {"Read" : 4} } )
> db.media.find ( { "Title" : "One Piece" } )
{
"Type" : "Manga",
"Title" : "One Piece ",
"Volumes" : "612",
"Read" : "524"
}
设置字段的值
您可以使用$set操作符将字段的值设置为您指定的值。这适用于任何数据类型,如下例所示:
> db.media.update ( { "Title" : "Matrix, The" }, {$set : { Genre :
"Sci-Fi" } } )
这个代码片段将更新之前创建的文档中的流派,将其设置为Sci-Fi。
删除指定的字段
$unset操作符允许您删除给定的字段,如下例所示:
> db.media.update ( {"Title": "Matrix, The"}, {$unset : { "Genre" : 1 } } )
这个代码片段将从文档中删除Genre键及其值。
向指定字段追加值
$push操作符允许您将一个值添加到指定的字段中。如果该字段是一个现有的数组,那么该值将被添加。如果该字段尚不存在,则该字段将被设置为数组值。如果字段存在,但它不是一个数组,那么将引发一个错误条件。
首先将另一位作者添加到您的收藏条目中:
> db.media.update ( {"ISBN" : "978-1-4302-5821-6"}, {$push: { Author : "Griffin,
Stewie"} } )
下一个代码片段引发了一条错误消息,因为Title字段不是一个数组:
> db.media.update ( {"ISBN" : "978-1-4302-5821-6"}, {$push: { Title :
"This isn't an array"} } )
Cannot apply $push/$pushAll modifier to non-array
以下示例显示了文档在此期间的外观:
> db.media.find ( { "ISBN" : "978-1-4302-5821-6" } )
{
"Author" :
[
"Hows, David",
"Membrey, Peter",
"Plugge, Eelco",
"Griffin, Stewie",
],
"ISBN" : "978-1-4302-5821-6",
"Publisher" : "Apress",
"Title" : "Definitive Guide to MongoDB 2nd ed., The",
"Type" : "Book",
"_id" : ObjectId("4c436231c603000000007ed0")
}
在数组中指定多个值
使用数组时,\(push 操作符将把指定的值附加到给定的数组中,扩展存储在给定元素中的数据。如果您希望将几个单独的值添加到给定的数组中,可以使用可选的`\)each`修饰符,如下例所示:
> db.media.update( { "ISBN" : "978-1-4302-5821-6" }, { $push: { Author : { $each: ["Griffin, Peter", "Griffin, Brian"] } } } )
{
"Author" :
[
"Hows, David",
"Membrey, Peter",
"Plugge, Eelco",
"Hawkins, Tim",
"Griffin, Stewie",
"Griffin, Peter",
"Griffin, Brian"
],
"ISBN" : "978-1-4302-5821-6",
"Publisher" : "Apress",
"Title" : "Definitive Guide to MongoDB 2nd ed., The",
"Type" : "Book",
"_id" : ObjectId("4c436231c603000000007ed0")
}
可选地,当使用$each时,可以使用$slice操作符。这允许你在一个$push操作中限制数组中元素的数量。$slice接受负数或零。使用负数可以确保数组中只保留最后 n 个元素,而使用零会清空数组。注意,$slice操作符必须是$push操作符的第一个修饰符,这样才能起作用:
> db.media.update( { "ISBN" : "978-1-4302-5821-6" }, { $push: { Author : { $each: ["Griffin, Meg", "Griffin, Louis"], $slice: -2 } } } )
{
"Author" :
[
"Griffin, Meg",
"Griffin, Louis"
],
"ISBN" : "978-1-4302-5821-6",
"Publisher" : "Apress",
"Title" : "Definitive Guide to MongoDB 2nd ed., The",
"Type" : "Book",
"_id" : ObjectId("4c436231c603000000007ed0")
}
如您所见,$slice操作符确保了不仅两个新值被推入,数组中保存的数据也被限制为指定的值(2)。在处理固定大小的数组时,$slice操作符是一个很有价值的工具。
用$addToSet 向数组中添加数据
$addToSet操作符是另一个允许您向数组添加数据的命令。但是,只有当数据不在数组中时,该运算符才会将数据添加到数组中。这样看来,$addToSet不像$push。默认情况下,$addToSet操作符接受一个参数。然而,在使用 t $addToSet时,您可以使用$each操作符来指定额外的参数。下面的代码片段将作者Griffin, Brian添加到 authors 数组中,因为它还不在那里:
> db.media.update( { "ISBN" : "1-4302-3051-7" }, {$addToSet : { Author :
"Griffin, Brian" } } )
再次执行代码片段不会改变任何事情,因为作者已经在数组中了。
然而,要添加多个值,您应该采用不同的方法,并使用$each操作符:
> db.media.update( { "ISBN" : "1-4302-3051-7" }, {$addToSet : { Author :
{ $each : ["Griffin, Brian","Griffin, Meg"] } } } )
至此,我们这个曾经看起来整洁可信的文档,已经被改造成这样了:
{
"Author" :
[
"Hows, David",
"Membrey, Peter",
"Plugge, Eelco",
"Hawkins, Tim",
"Griffin, Stewie",
"Griffin, Peter",
"Griffin, Brian",
"Griffin, Louis",
"Griffin, Meg"
],
"ISBN" : "1-4302-3051-7",
"Publisher" : "Apress",
"Title" : "Definitive Guide to MongoDB, The",
"Type" : "Book",
"_id" : ObjectId("4c436231c603000000007ed0")
}
从数组中移除元素
MongoDB 还包括几个方法,可以让你从数组中移除元素,包括$pop、$pull、$pullAll。在接下来的小节中,您将学习如何使用这些方法从数组中移除元素。
$pop操作符允许您从数组中删除单个元素。该运算符允许您删除数组中的第一个或最后一个值,这取决于您传递给它的参数。例如,下面的代码片段删除了数组中的最后一个元素:
> db.media.update( { "ISBN" : "1-4302-3051-7" }, {$pop : {Author : 1 } } )
在这种情况下,$pop操作符会将 Meg 的名字从作者列表中弹出。传递一个负数会从数组中移除第一个元素。以下示例从作者列表中删除 Peter Membrey 的名字:
> db.media.update( { "ISBN" : "1-4302-3051-7" }, {$pop : {Author : -1 } } )
Note
指定值-2或1000不会改变删除哪个元素。任何负数都将删除第一个元素,而任何正数都将删除最后一个元素。使用数字0从数组中删除最后一个元素。
移除指定值的每个匹配项
$pull操作符允许您从数组中删除指定值的每一次出现。如果数组中有多个值相同的元素,这可能特别有用。让我们从使用$push参数将 Stewie 添加回作者列表开始这个例子:
> db.media.update ( {"ISBN" : "1-4302-3051-7"}, {$push: { Author :
"Griffin, Stewie"} } )
当我们浏览这本书的例子时,Stewie 将多次进出数据库。您可以使用以下代码删除文档中出现的该作者的所有内容:
> db.media.update ( {"ISBN" : "1-4302-3051-7"}, {$pull : { Author : "Griffin,
Stewie" } } )
从数组中删除多个元素
您也可以从数组中移除多个具有不同值的元素。$pullAll操作符使您能够完成这个任务。$pullAll操作符接受一个包含所有要删除的元素的数组,如下例所示:
> db.media.update( { "ISBN" : "1-4302-3051-7"}, {$pullAll : { Author :
["Griffin, Louis","Griffin, Peter","Griffin, Brian"] } } )
从中移除元素的字段(上例中的Author)需要是一个数组。如果不是,您将收到一条错误消息。
指定匹配数组的位置
您可以在查询中使用$操作符来指定查询中匹配数组项的位置。找到数组成员后,可以使用该运算符进行数据操作。例如,假设您在曲目列表中添加了另一首曲目,但在输入曲目编号时不小心打错了:
> db.media.update( { "Title" : "Nirvana" }, {$addToSet : { Tracklist :
{"Track" : 2,"Title": "Been a Son", "Length":"2:23"} } } )
{
"Artist" : "Nirvana",
"Title" : "Nirvana",
"Tracklist" : [
{
"Track" : "1",
"Title" : "You Know You're Right",
"Length" : "3:38"
},
{
"Track" : "5",
"Title" : "Smells Like Teen Spirit",
"Length" : "5:02"
},
{
"Track" : 2,
"Title" : "Been a Son",
"Length" : "2:23"
}
],
"Type" : "CD",
"_id" : ObjectId("4c443ad6c603000000007ed5")
}
碰巧你知道最近项目的轨道号应该是3而不是2。您可以将$inc方法与$操作符结合使用,将值从2增加到3,如下例所示:
> db.media.update( { "Tracklist.Title" : "Been a son"},
{$inc:{"Tracklist.$.Track" : 1} } )
请注意,只有它匹配的第一个项目才会被更新。因此,如果 comments 数组中有两个相同的元素,那么只有第一个元素会增加。
原子操作
MongoDB 支持对单个文档执行原子操作。原子操作是一组操作,这些操作可以以这样的方式组合,使得这组操作对于系统的其余部分来说看起来仅仅是一个操作。这组操作的最终结果要么是正面的,要么是负面的。
如果一组操作满足以下一对条件,则可以将其称为原子操作:
No other process knows about the changes being made until the entire set of operations has completed. If one of the operations fails, the entire set of operations (the entire atomic operation) will fail, resulting in a full rollback, where the data is restored to its state prior to running the atomic operation.
执行原子操作时的一个标准行为是数据将被锁定,因此其他查询无法访问。但是,MongoDB 不支持锁定或复杂事务,原因有很多:
- 在分片环境中(参见第 12 章了解更多关于这种环境的信息),分布式锁可能是昂贵且缓慢的。MongoDB 的目标是轻量级和快速,所以昂贵和缓慢违背了原则。
- MongoDB 开发者不喜欢死锁的想法。在他们看来,系统最好是简单且可预测的。
- MongoDB 旨在很好地解决实时问题。当执行锁定大量数据的操作时,它还会在较长时间内停止一些较小的轻型查询。同样,这违背了 MongoDB 的速度目标。
MongoDB 包括几个更新操作符(如前所述),所有这些操作符都可以自动更新元素:
$set:设定特定值。$unset:删除特定值。$inc:将特定值增加一定的量。$push:向数组追加一个值。$pull:从现有数组中删除一个或多个值。$pullAll:从现有数组中删除几个值。
使用“如果当前更新”方法
原子更新使用的另一个策略是 update-if-current。该方法采取以下三个步骤:
It fetches the object from the document. It modifies the object locally (with any of the previously mentioned operations, or a combination of them). It sends an update request to update the object to the new value, in case the current value still matches the old value fetched.
你可以使用getlasterror方法来检查是否一切顺利。请注意,所有这些都是自动发生的。让我们重新看一下之前显示的示例:
> db.media.update( { "Tracklist.Title" : "Been a son"},
{$inc:{"Tracklist.$.Track" : 1} } )
现在您可以使用getlasterror命令来检查更新是否顺利进行:
> db.$cmd.findOne({getlasterror:1})
如果原子更新成功执行,您将获得以下结果:
{ "err" : null, "updatedExisting" : true, "n" : 1, "ok" : 1 }
在本例中,您使用曲目列表标题作为标识符来增加Tracklist.Track。但是现在考虑一下,如果在 MongoDB 修改您的数据时,另一个用户使用相同的方法更改了曲目列表数据,会发生什么情况。因为Tracklist.Title保持不变,所以您可能会(错误地)认为您正在更新原始数据,而实际上您正在覆盖这些更改。
这就是众所周知的 ABA 问题。这种情况似乎不太可能,但是在多用户环境中,许多应用同时处理数据,这可能是一个严重的问题。
要避免此问题,您可以执行下列操作之一:
- 在更新的查询表达式中使用整个对象,而不仅仅是
_id和comments.by字段。 - 使用
$set设置您关心的字段。如果其他领域发生了变化,也不会受此影响。 - 在对象中放置一个版本变量,并在每次更新时递增。
- 如果可能的话,使用一个
$操作符来代替更新当前操作序列。
Note
MongoDB 不支持在单个操作中自动更新多个文档。相反,您可以使用嵌套对象,这有效地使它们成为一个用于原子目的的文档。
原子地修改和返回文档
findAndModify命令还允许您对文档执行原子更新。这个命令修改文档并返回它。这个命令有三个主要的操作符:<query>,用来指定执行命令所针对的文档;<sort>,用于在多个匹配时对匹配的文档进行排序,以及<operations>,用于指定需要做什么。
现在让我们来看几个说明如何使用这个命令的例子。第一个示例查找您要搜索的文档,并在找到后删除它:
> db.media.findAndModify( { "Title" : "One Piece",sort:{"Title": -1}, remove:
true} )
{
"_id" : ObjectId("4c445218c603000000007ede"),
"Type" : "Manga",
"Title" : "One Piece",
"Volumes" : 612,
"Read" : 524
}
这段代码返回它找到的符合标准的文档。在本例中,它找到并删除了标题为“One Piece”的第一个项目。如果您现在执行一个find()函数,您将看到该文档不再位于集合中。
下一个示例修改文档,而不是删除它:
> db.media.findAndModify( { query: { "ISBN" : "987-1-4302-3051-9" }, sort:
{"Title":-1}, update: {$set: {"Title" : " Different Title"} } } )
前面的例子将标题从“Definitive Guide to MongoDB”更新为“Different Title”,并将旧文档(与更新前一样)返回到 shell。如果您希望看到文档的更新结果,可以在查询后添加new操作符:
> db.media.findAndModify( { query: { "ISBN" : "987-1-4302-3051-9" }, sort:
{"Title":-1}, update: {$set: {"Title" : " Different Title"} }, new:true } )
注意,你可以用这个命令使用任何修饰操作,不仅仅是$set。
重命名收藏
可能会发生这样的情况:您发现自己给一个集合命名不正确,但是您已经向其中插入了一些数据。这可能会使从零开始删除和再次读取数据变得很麻烦。
相反,您可以使用renameCollection()函数来重命名您现有的收藏。以下示例向您展示了如何使用这个简单明了的命令:
> db.media.renameCollection("newname")
{ "ok" : 1 }
如果命令执行成功,将返回一个OK。但是,如果失败(例如,如果集合不存在),则返回以下消息:
{ "errmsg" : "assertion: source namespace does not exist", "ok" : 0 }
renameCollection命令不带很多参数(不像你目前看到的一些命令);然而,在适当的情况下,它会非常有用。
删除数据
到目前为止,我们已经探索了如何添加、搜索和修改数据。接下来,我们将研究如何删除文档、整个集合以及数据库本身。
之前,您学习了如何从特定文档中删除数据(例如,使用$pop命令)。在本节中,您将学习如何删除完整的文档和集合。正如insert()功能用于插入,update()用于修改文档一样,remove()用于删除文档。
若要从收藏中删除单个文档,您需要指定用于查找该文档的条件。一个好的方法是先执行一个find();这确保了所使用的标准特定于您的文档。一旦确定了标准,就可以使用该标准作为参数来调用remove()函数:
> db.newname.remove( { "Title" : "Different Title" } )
此语句删除之前添加的图书或您的收藏中任何其他同名的项目。该语句删除了该书名下的所有书籍,这也是为什么最好指定项目的_id值的原因之一——它总是唯一的。
或者您可以使用下面的代码片段从newname库中删除所有文档(记住,我们之前已经将这个集合重命名为media):
> db.newname.remove({})
Warning
当删除一个文档时,您需要记住对该文档的任何引用都将保留在数据库中。因此,请确保手动删除或更新这些引用;否则,这些引用在计算时将返回 null。引用将在下一节讨论。
如果您想删除整个收藏,您可以使用drop()功能。下面的代码片段删除了整个newname集合,包括它的所有文档:
> db.newname.drop()
true
根据操作是否成功完成,drop()函数返回true或false。同样,如果想从 MongoDB 中删除整个数据库,可以使用dropDatabase()函数,如下例所示:
> db.dropDatabase()
{ "dropped" : "library", "ok" : 1 }
请注意,这个代码片段将删除您当前正在工作的数据库(同样,一定要检查db以查看哪个数据库是您当前的数据库)。
引用数据库
此时,您又有了一个空数据库。您还熟悉向集合中插入各种数据。现在,您已经准备好进一步学习数据库引用。正如您已经看到的,在很多情况下,将数据嵌入到您的文档中就足以满足您的应用(例如图书条目中的曲目列表或作者列表)。但是,有时您确实需要引用另一个文档中的信息。以下部分将解释如何着手这样做。
与 SQL 一样,MongoDB 中文档之间的引用是通过在服务器上执行额外的查询来解决的。MongoDB 提供了两种方法来实现这一点:手动引用它们或者使用 DBRef 标准,许多驱动程序也支持 db ref 标准。
手动引用数据
引用数据最简单、最直接的方法是手动操作。当手动引用数据时,通过完整的 ID 或更简单的通用术语,将来自另一个文档的_id的值存储在您的文档中。在继续示例之前,让我们添加一个新文档,并在其中指定发布者的信息(密切注意_id字段:
> apress = ( { "_id" : "Apress", "Type" : "Technical Publisher", "Category" :
["IT", "Software","Programming"] } )
{
"_id" : "Apress",
"Type" : "Technical Publisher",
"Category" : [
"IT",
"Software",
"Programming"
]
}
> db.publisherscollection.insert(apress)
一旦添加了出版商的信息,就可以将实际的文档(例如,一本书的信息)添加到media集合中了。下面的示例添加一个文档,并将Apress指定为发布者的名称:
> book = ( { "Type" : "Book", "Title" : "Definitive Guide to MongoDB 2nd ed., The",
"ISBN" : "987-1-4302-5821-6", "Publisher" : "Apress","Author" : ["Hows, David",""Plugge, Eelco","Membrey,Peter",Hawkins, Tim"] } )
{
"Type" : "Book",
"Title" : "Definitive Guide to MongoDB 2nd ed., The",
"ISBN" : "987-1-4302-5821-6",
"Publisher": "Apress",
"Author" : [
"Hows, David"
"Membrey, Peter",
"Plugge, Eelco",
" Hawkins, Tim"
]
}
> db.media.insert(book)
您需要的所有信息已经分别插入到了publisherscollection和media集合中。您现在可以开始使用数据库参考。首先,将包含发布者信息的文档指定给一个变量:
> book = db.media.findOne()
{
"_id" : ObjectId("4c458e848e0f00000000628e"),
"Type" : "Book",
"Title" : "Definitive Guide to MongoDB, The",
"ISBN" : "987-1-4302-3051-9",
"Publisher" : "Apress",
"Author" : [
"Hows, David"
"Membrey, Peter",
"Plugge, Eelco",
"Hawkins, Tim"
]
}
为了获得信息本身,您将findOne函数与一些点符号结合起来:
> db.publisherscollection.findOne( { _id : book.Publisher } )
{
"_id" : "Apress",
"Type" : "Technical Publisher",
"Category" : [
"IT",
"Software",
"Programming"
]
}
如本例所示,手动引用数据非常简单,不需要太多脑力劳动。这里,users集合中的文档中的_id是手工设置的,不是由 MongoDB 生成的(否则,_id将是一个对象 ID)。
使用 DBRef 引用数据
DBRef 标准为文档间引用数据提供了更正式的规范。使用 DBRef 而不是手动引用的主要原因是,集合可以从一个文档改变到下一个文档。因此,如果您引用的集合总是相同的,那么手动引用数据(如前所述)就可以了。
使用 DBRef,数据库引用被存储为标准的嵌入式(JSON/BSON)对象。用一种标准的方式来表示引用意味着驱动程序和数据框架可以添加以标准方式操作引用的助手方法。
添加 DBRef 引用值的语法如下所示:
{ $ref :``<collectionname>``, $id :``<id value>``[, $db :``<database name>
这里,<collectionname>表示引用的集合的名称(例如,publisherscollection);<id value>代表您正在引用的对象的_id字段的值;可选的$db允许您引用其他数据库中的文档。
让我们从头开始看另一个使用 DBRef 的例子。首先清空您的两个收藏并添加一个新文档:
> db.publisherscollection.drop()
true
> db.media.drop()
true
> apress = ( { "Type" : "Technical Publisher", "Category" :
["IT","Software","Programming"] } )
{
"Type" : "Technical Publisher",
"Category" : [
"IT",
"Software",
"Programming"
]
}
> db.publisherscollection.save(apress)
到目前为止,您已经定义了变量apress并使用save()函数保存了它。接下来,通过键入变量名称来显示变量的更新内容:
> apress
{
"Type" : "Technical Publisher",
"Category" : [
"IT",
"Software",
"Programming"
],
"_id" : ObjectId("4c4597e98e0f000000006290")
}
到目前为止,您已经定义了发布者并将其保存到了publisherscollection集合中。现在,您已经准备好向引用数据的媒体集合添加一个项目:
> book = { "Type" : "Book", "Title" : "Definitive Guide to MongoDB 2nd ed., The",
"ISBN" : "978-1-4302-5821-6", "Author": ["Hows, David","Membrey, Peter","Plugge,
Eelco","Hawkins, Tim"], Publisher : [ new DBRef ('publisherscollection',
apress._id) ] }
{
"Type" : "Book",
"Title" : "Definitive Guide to MongoDB 2nd ed., The",
"ISBN" : "987-1-4302-5821-6",
"Author" : [
"Hows, David"
"Membrey, Peter",
"Plugge, Eelco",
"Hawkins, Tim"
],
"Publisher" : [
DBRef("publishercollection", "Apress")
]
}
> db.media.save(book)
就这样!当然,这个例子看起来没有引用数据的手工方法简单;但是,对于集合可能从一个文档变化到下一个文档的情况,这是一个很好的选择。
实现与索引相关的功能
在前一章中,您简要地看了一下索引能为您的数据库做些什么。现在是时候简要学习如何创建和使用索引了。索引将在第 10 章中更详细地讨论,但是现在让我们看看基础知识。MongoDB 包含了相当多的用于维护索引的函数;我们将从用ensureIndex()函数创建一个索引开始。
ensureIndex()函数至少接受一个参数,这个参数是您将用来构建索引的一个文档中的一个键的名称。在前面的例子中,您向使用了Title键的media集合添加了一个文档。这个键上的索引将很好地服务于这个集合。
Tip
MongoDB 的经验法则是为你想在 MySQL 中创建的相同场景创建一个索引。
您可以通过调用以下命令为此集合创建索引:
> db.media.ensureIndex( { Title : 1 } )
该命令确保为来自media集合中所有文档的所有Title值创建一个索引。行尾的:1指定了索引的方向:1按升序存储项目,而-1按降序存储项目。
// Ensure ascending index
db.media.ensureIndex( { Title :1 } )
// Ensure descending index
db.media.ensureIndex( { Title :-1 } )
Tip
搜索索引信息的速度很快。搜索非索引信息很慢,因为每个文档都需要检查是否匹配。
BSON 允许你在一个文档中存储完整的数组;然而,能够在嵌入式键上创建索引也是有益的。幸运的是,MongoDB 的开发者也想到了这一点,并增加了对这一特性的支持。让我们以本章前面的一个例子为基础,将另一个嵌入了信息的文档添加到数据库中:
> db.media.insert( { "Type" : "CD", "Artist" : "Nirvana","Title" :
"Nevermind", "Tracklist" : [ { "Track" : "1", "Title" : "Smells Like Teen
Spirit", "Length" : "5:02" }, {"Track" : "2","Title" : "In Bloom", "Length" :
"4:15" } ] } )
{ "_id" : ObjectId("4c45aa2f8e0f000000006293"), "Type" : "CD", "Artist" :
"Nirvana", "Title" : "Nevermind", "Tracklist" : [
{
"Track" : "1",
"Title" : "Smells Like Teen Spirit",
"Length" : "5:02"
},
{
"Track" : "2",
"Title" : "In Bloom",
"Length" : "4:15"
}
] }
接下来,您可以在Title键上为曲目列表中的所有条目创建索引:
> db.media.ensureIndex( { "Tracklist.Title" : 1 } )
下一次你搜索收藏中的任何一个标题时——假设它们嵌套在Tracklist下——这些标题会立即显示出来。接下来,您可以进一步发展这个概念,使用整个(子)文档作为键,如下例所示:
> db.media.ensureIndex( { "Tracklist" : 1 } )
该语句索引数组的每个元素,这意味着您现在可以搜索数组中的任何对象。这些类型的键也称为多键。您还可以基于一组文档中的多个关键字创建索引。这个过程被称为复合索引。您用来创建复合索引的方法基本相同;不同之处在于您指定了几个键,而不是一个,如下例所示:
> db.media.ensureIndex({"Tracklist.Title": 1, "Tracklist.Length": -1})
这种方法的好处是可以在多个键上建立索引(就像前面的例子一样,可以索引整个子文档)。但是,与子文档方法不同,复合索引允许您指定是否希望两个字段中的一个按降序进行索引。如果使用子文档方法执行索引,则只能按升序或降序排列。第 10 章中有更多关于复合索引的内容。
与测量索引相关的命令
到目前为止,您已经快速浏览了一个与索引相关的命令,ensureIndex()。毫无疑问,这是您将主要用来创建索引的命令。然而,您可能还会发现一对有用的附加函数:hint()和min()/max()。您可以使用这些函数来查询数据。到目前为止,我们还没有介绍它们,因为没有自定义索引它们就无法工作。但是现在让我们来看看他们能为你做什么。
强制指定的索引查询数据
您可以使用hint()函数在查询数据时强制使用指定的索引。使用该命令的预期好处是提高查询性能。要了解这一原理的实际应用,请尝试在不定义索引的情况下使用hint()函数执行find:
> db.media.find( { ISBN: " 978-1-4302-5821-6"} ) . hint ( { ISBN: -1 } )
error: { "$err" : "bad hint", "code" : 10113 }
如果你在 ISBN 号上创建一个索引,这个技术会更成功。注意,第一个命令的background参数确保索引是在后台完成的:
> db.media.ensureIndex({ISBN: 1}, {background: true});
> db.media.find( { ISBN: " 978-1-4302-5821-6"} ) . hint ( { ISBN: 1 } )
{ "_id" : ObjectId("4c45a5418e0f000000006291"), "Type" : "Book", "Title" : "Definitive Guide to MongoDB, The", "ISBN" : " 978-1-4302-5821-6", "Author" : ["Hows, David","Membrey, Peter", "Plugge, Eelco","Hawkins,Tim"], "Publisher" : [
{
"$ref" : "publisherscollection",
"$id" : ObjectId("4c4597e98e0f000000006290")
}
] }
为了确认给定的索引正在被使用,您可以选择添加explain()函数,返回关于所选查询计划的信息。这里,indexBounds值告诉您所使用的索引:
> db.media.find( { ISBN: " 978-1-4302-5821-6"} ) . hint ( { ISBN: 1 } ).explain()
{
"cursor" : "BtreeCursor ISBN_1",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"ISBN" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
]
},
"server" : "localhost:27017"
}
约束查询匹配
min()和max()函数使您能够将查询匹配限制为那些索引键在指定的最小和最大键之间的匹配。因此,您需要为指定的键建立一个索引。此外,您可以将这两个功能结合使用,也可以单独使用。让我们首先添加一些文档,使您能够利用这些功能。首先,在Released字段上创建一个索引:
> db.media.insert( { "Type" : "DVD", "Title" : "Matrix, The", "Released" :
1999} )
> db.media.insert( { "Type" : "DVD", "Title" : "Blade Runner", "Released" :
1982 } )
> db.media.insert( { "Type" : "DVD", "Title" : "Toy Story 3", "Released" :
2010} )
> db.media.ensureIndex( { "Released": 1 } )
您现在可以使用max()和min()命令,如下例所示:
> db.media.find() . min ( { Released: 1995 } ) . max ( { Released : 2005 } )
{ "_id" : ObjectId("4c45b5b38e0f0000000062a9"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999 }
如果没有创建索引,那么将返回一条错误消息,指出没有为指定的键模式找到索引。显然,您需要定义哪个索引必须与hint()函数一起使用:
> db.media.find() . min ( { Released: 1995 } ) .
max ( { Released : 2005 } ). hint ( { Released : 1 } )
{ "_id" : ObjectId("4c45b5b38e0f0000000062a9"), "Type" : "DVD", "Title" :
"Matrix, The", "Released" : 1999 }
Note
min()值将包含在结果中,而max()值将从结果中排除。
一般来说,建议您使用$gt和$lt(分别为大于和小于),而不是min()和max(),因为$gt和$lt不需要索引。min()和max()功能主要用于复合按键。
摘要
在本章中,我们已经了解了可以用 MongoDB shell 来操作数据的最常用的命令和选项。我们还研究了如何搜索、添加、修改和删除数据,以及如何修改您的集合和数据库。接下来,我们快速地看了一下原子操作,如何使用聚合,以及何时使用像$elemMatch这样的操作符。最后,我们探讨了如何创建索引以及何时使用它们。我们研究了索引的用途,如何删除索引,如何使用创建的索引搜索数据,以及如何检查正在运行的索引操作。
在下一章中,我们将研究 GridFS 的基础知识,包括它是什么,它做什么,以及如何使用它为您带来好处。
五、文件系统
Abstract
我们生活在一个高清视频、1200 万像素摄像头和可以在光盘大小的光盘上存储 50GB 数据的存储介质的世界。在这种情况下,MongoDB 文档的最大大小限制为 16MB 可能显得不够。事实上,您可能想知道为什么 MongoDB,这个被设计为当今高科技时代的数据库,有这样一个看似奇怪的限制。简单的答案是性能。
我们生活在一个高清视频、1200 万像素摄像头和可以在光盘大小的光盘上存储 50GB 数据的存储介质的世界。在这种情况下,MongoDB 文档的最大大小限制为 16MB 可能显得不够。事实上,您可能想知道为什么 MongoDB,这个被设计为当今高科技时代的数据库,有这样一个看似奇怪的限制。简单的答案是性能。
如果数据存储在文档本身中,它显然会变得非常大,这反过来会使数据更难处理。例如,拉回整个文档也需要加载文档中的文件。您可以解决这个问题,但是无论何时访问它,您仍然需要拉回整个文件,即使您只想要其中的一小部分。你不能在文档中间要求一大块数据——这是一个要么全有要么全无的命题。幸运的是,MongoDB 为这个问题提供了一个独特而优雅的解决方案。MongoDB 使您能够非常容易地存储大文件,但它也允许您访问文件的一部分,而不必检索整个文件——同时保持高性能。它通过利用一种称为 GridFS 的规范来实现这一点。
Note
关于 GridFS 的一个有趣的事情是,它实际上不是一个软件特性。例如,MongoDB 中没有任何管理 GridFS 的特殊服务器端代码。相反,GridFS 是一个简单的规范,MongoDB 上所有受支持的驱动程序都使用它。这种规范的主要好处是,一个驱动程序存储的文件可以被遵循相同约定的任何其他驱动程序访问。
这种方法严格遵循 MongoDB 保持简单的原则。因为 GridFS 使用标准的 MongoDB 特性,所以从驱动程序的角度来看,很容易实现和使用该规范。这也意味着如果你真的想的话,你可以用手去摸索,因为 GridFS 规范中的 MongoDB 文件只是包含文档的普通集合。
填充一些背景
第一章提到了这样一个事实,多年来我们一直被教导使用数据库进行简单的存储。例如,我们中的一个人在 15 年前买了一本书来帮助提高他的 PHP,这本书在第三章第一节介绍了 MySQL。考虑到现实世界中 SQL 和数据库的复杂性(更不用说理论上了),您可能想知道为什么一本面向初学者的书实际上是从 SQL 开始的。毕竟,这是一本 PHP 书籍,而不是 MySQL 书籍。
有一件事大多数人直到尝试过之后才意识到,那就是直接在磁盘上读写数据是很难的。在这一点上,有些人不同意我们的观点——毕竟,用 Python 打开和读取文件可能看起来微不足道。事实就是:在更简单的场景中,使用 PHP 处理文件是相当容易的。如果你想做的只是读入行并处理它们,你不太可能有任何麻烦。
另一方面,如果您想要搜索文件或存储复杂或结构化的数据,事情会变得困难得多。即使您能够解决这个问题并创建一个解决方案,您的解决方案也不可能比依赖数据库更快或更有效。今天的应用依赖于快速查找和存储数据——对于我们这些不能或不想自己编写这样一个系统的人来说,数据库使这成为可能。
许多书都忽略了一个领域,那就是文件的存储。大多数教你使用数据库存储数据的书也教你在需要存储文件时读写文件系统。在某些方面,这通常不是问题,因为读写简单的文件比处理其中的内容要容易得多。然而,还是有一些问题。首先,开发者必须首先拥有写这些文件的权限,这需要给 web 服务器写本地文件系统的权限。这看起来不太可能造成问题,但是它给系统管理员带来了噩梦——将文件加载到服务器上是能够破坏它的第一步。
数据库可以存储二进制文件;通常,他们这样做并不优雅。MySQL 有一个特殊的列类型叫做BLOB。PostgreSQL 要求遵循特殊的过程来存储这样的文件——并且数据不存储在表本身中。换句话说,就是乱。这些解决方案显然是附加的。因此,人们选择将数据写入磁盘就不足为奇了。但是这种方法也有问题。除了安全性问题之外,它还增加了另一个需要备份的目录,并且您还必须确保这些信息被复制到所有适当的服务器上。有些文件系统提供了写入磁盘并完全复制内容的能力(包括 GFS);但是这些解决方案很复杂并且增加了开销;此外,这些特性通常会使您的解决方案更难维护。
另一方面,MongoDB 强制规定最大文档大小为 16MB。对于存储富文档来说,这已经足够了,而且在几年前,对于存储许多其他类型的文件来说,这也已经足够了。然而,这个限制对于今天的环境是完全不够的。
使用 GridFS
接下来,我们将简要了解 GridFS 是如何实现的。正如 MongoDB 网站所指出的,使用它不需要理解或了解 GridFS 的底层实现。事实上,你可以简单地让司机为你处理重物。在很大程度上,支持 GridFS 的驱动程序以特定于语言的方式实现文件处理。例如,Python 的 MongoDB 驱动程序的工作方式与 Python 完全一致,您很快就会看到这一点。如果您对 GridFS 的详细内容不感兴趣,那么直接跳到下一节。我们保证你不会错过任何能让你有效使用 MongoDB 的东西!
GridFS 由两部分组成。更确切地说,它由两个集合组成。一个集合保存文件名和相关信息,如大小(称为元数据),而另一个集合保存文件数据本身,通常是 256K 的块。规范要求将它们分别命名为files和chunks。默认情况下,files和chunks集合是在fs命名空间中创建的,但是这是可以改变的。如果您想要存储不同类型的文件,更改默认命名空间的功能非常有用。例如,您可能希望将图像和电影文件分开。
命令行工具入门
现在我们已经了解了一些背景知识,让我们看看如何通过探索可利用的命令行工具来开始使用 GridFS。首先,我们需要一个文件来玩。为了简单起见,让我们使用字典文件。在 Ubuntu 上,你可以在/usr/share/dict/words找到这个。但是,符号链接有不同的级别,因此您可能希望首先运行以下命令:
root@core2:/usr/share/dict# cat words > /tmp/dictionary
Note
在 Ubuntu 中,你可能需要使用apt-get install wbritish来安装字典文件。
这个命令将文件的所有内容复制到一个简单明了的路径中,您可以很容易地使用它。当然,对于这个例子,您可以使用您希望的任何文件;它不需要任何特定的大小或类型。
与其描述您可以使用mongofiles的所有选项,不如让我们直接开始使用该工具的一些特性。这本书假设您在与 MongoDB 相同的机器上运行mongofiles。如果不是,那么您需要使用–h选项来指定运行 MongoDB 的主机。在测试完mongofiles命令后,您将了解到它的其他可用选项。
首先,让我们列出数据库中的所有文件。我们不希望有任何文件在那里,但让我们确定一下。list命令列出了到目前为止数据库中的文件:
$ mongofiles list
connected to: 127.0.0.1
$
好吧,那可能不是很令人兴奋。请记住,mongofiles是一个概念验证工具;在您自己的应用中,这可能不是一个常用的工具。但是,mongofiles对于学习和测试来说是很棒的。一旦创建了一个文件,就可以使用该工具来浏览所创建的文件和块。
让我们更进一步,使用put命令添加之前创建的字典文件(记住:在这个例子中,您可以使用您喜欢的任何文件):
$ mongofiles put /tmp/dictionary
connected to: 127.0.0.1
added file: { _id: ObjectId('51cb61b26487b3d8ce7af440'), filename: "/tmp/dictionary", chunkSize: 262144, uploadDate: new Date(1372283314621), md5: "40c0825855792bd20e8a2d515fe9c3e3", length: 4953699 }}}
done!
$
此示例返回一些有用的信息;但是,让我们通过确认文件是否存在来仔细检查它显示的信息。通过重新运行list命令来完成:
$ mongofiles list
connected to: 127.0.0.1
/tmp/dictionary 4953699
$
这个例子显示了字典文件及其大小。这些信息显然来自于files系列,但是我们正在超越自己。让我们花点时间回过头来,检查一下本例中从put命令返回的输出。
使用 _id 键
如您所知,MongoDB 中的每个文档都包含一个存储在_id键中的惟一标识符。像 MySQL 的auto_increment字段一样,_id键没有太大的直接意义,除了它允许你选择一个特定的文件。
使用文件名
put命令的输出还显示了一个Filename键,这本身需要一点解释。通常,您会希望保持该字段的唯一性,以帮助防止重大混淆;然而,这并不完全必要。事实上,如果您再次运行put命令,您将得到两个看起来完全相同的文档。在这种情况下,除了_id键之外,文件和元数据是相同的。您可能会对此感到惊讶,并想知道为什么 MongoDB 不更新现有的文件,而是创建一个新文件。原因是,在许多情况下,您可能会有相同的文件名。例如,如果你建立了一个系统来存储学生的作业,那么很有可能至少有一些文件名是相同的。MongoDB 不能假设相同的文件名(即使是大小相同的文件名)实际上是同一个文件。因此,在很多情况下,MongoDB 更新文件是错误的。当然,你可以使用_id键来更新一个特定的文件;在接下来的基于 Python 的实验中,您将了解到更多关于这个主题的内容。
确定文件的长度
put命令还返回文件的长度,这既是有用的信息,也是 GridFS 如何工作的关键。虽然知道一个文件有多大是很好的参考,但是当您编写自己的应用时,文件的大小也起着很大的作用。例如,当通过 Web(例如,通过 HTTP)发送文件时,您需要指定文件有多大。不是所有的服务器都这样做;例如,当从某些网站下载文件时,您可能已经注意到您的浏览器可以告诉您下载文件的速度,但不能告诉您完成文件下载需要多长时间。这是因为服务器没有提供大小信息。
了解文件的大小在另一方面也很重要。前面,我们提到文件被分解成块,也就是说,文件被分割成更小的部分。默认情况下,块大小是 256K,但是如果您愿意,可以将其更改为另一个值。要计算出一个文件占用了多少块,你需要知道两件事。首先你必须知道每块有多大;第二,你必须知道文件的大小,这样你才能知道有多少块。
你可能认为这不重要。毕竟,如果您有一个 1MB 的文件,块大小是 256K,那么您知道如果您想要访问从 800K 标记开始的数据,您必须从第四个块开始。然而,您仍然需要知道整个文件有多大,原因如下:如果您不知道大小,您就无法计算出有多少个有效的块。在前面的例子中,没有什么可以阻止您请求从 1.26MB 开始的数据(即第六个块)。在这种情况下,该块不存在,但是如果不参考文件大小,就无法知道这一点。当然,驱动程序会为您处理所有这些事情,因此您无需对此过于担心;然而,在调试应用时,了解 GridFS 的“幕后”工作方式肯定会有所帮助。
使用区块大小
put命令也返回块大小,因为尽管有一个默认的块大小,但这个默认大小可以逐个文件地更改。这允许灵活的规模。如果你的网站流视频,你可能想有许多块,以便你可以很容易地跳到一个给定的视频的任何部分。如果你有一个大文件,你必须返回整个文件,然后在其中找到指定部分的起始点。使用 GridFS,您可以在块级别拉回数据。如果您使用默认大小,那么您可以开始从任何 256K 的块中检索数据。当然,您也可以指定您实际需要的数据比特(例如,您可能在一部 60 分钟的电影中间只需要 5 分钟)。这是一个非常高效的系统,对于大多数用途来说,256K 是一个非常好的块大小。如果你决定改变它,你应该有一个好的理由。和往常一样,不要忘记对定制块大小的性能进行基准测试;理论上更好的系统达不到预期并不罕见。
Note
MongoDB 对文档大小有 16MB 的限制。因为 GridFS 只是标准 MongoDB 框架中存储文件的一种不同方式,所以 GridFS 中也存在这种限制。也就是说,您不能创建大于 16MB 的块。这不应该成为问题,因为 GridFS 的全部目的是缓解对大文档的需求。如果您担心您正在存储巨大的文件,这会给您带来太多的大块文档,您不必担心——生产中的 MongoDB 系统有超过 10 亿个文档!
跟踪上传日期
uploadDate键正如它的名字所暗示的那样:它在 MongoDB 中存储文件的创建日期。这是一个提及files集合只是一个普通的 MongoDB 集合,包含普通文档的好时机。这意味着您可以添加您需要的任何额外的键和值对,就像您对任何其他集合所做的那样。
例如,考虑一个现实世界的应用,它需要存储从各种文件中提取的文本内容。您可能需要这样做,以便执行一些额外的索引和搜索。为此,您可以添加一个file_text键并将文本存储在那里。GridFS 系统的优雅意味着您可以使用该系统做任何事情,就像使用任何其他 MongoDB 文档一样。优雅和强大是在 MongoDB 中工作的两个决定性特征。
散列你的文件
MongoDB 附带了 MD5 散列算法。你可能以前在网上下载软件时遇到过这种算法。MD5 背后的理论是每个文件都有一个唯一的签名。在该文件中的任何地方改变一个比特都将彻底地(并且显著地)改变签名。使用这种签名有两个原因:安全性和完整性。为了安全起见,如果您知道 MD5 散列应该是什么,并且您信任其来源(可能是一个朋友给您的),那么如果散列(通常称为校验和)是正确的,您就可以确信文件没有被修改。这也确保了文件的完整性得到维护,并且没有数据丢失或损坏。特定文件的 MD5 哈希就像文件的指纹。哈希还可以用于识别文件名不同但内容相同的文件。
Warning
MD5 算法不再被认为是安全的,并且已经证明可以创建具有相同 MD5 校验和的两个不同文件,即使它们的内容不同。用密码术语来说,这叫做冲突。这种冲突是有害的,因为这意味着攻击者有可能以无法检测到的方式修改文件。这个警告在某种程度上仍然是理论上的,因为有意制造这样的碰撞需要大量的努力和时间;即使这样,这些文件也可能如此不同,以至于显然不是同一个文件。因此,MD5 仍然是确定文件完整性的首选方法,因为它得到了广泛的支持。但是,如果您希望使用散列来获得其安全性好处,那么您最好使用 SHA 系列规范之一—理想情况下是 SHA-256 或 SHA-512。甚至这些散列族也有一些理论上的漏洞;然而,还没有人展示过为 SHA 散列族制造有意碰撞的实际案例。MongoDB 使用 MD5 来确保文件的完整性,这对于大多数目的来说是很好的。但是,如果您想要散列重要的数据(比如用户密码),您可能应该考虑使用 SHA 散列族。
在 MongoDB 的引擎盖下寻找
此时,您在 MongoDB 数据库中有了一些数据。现在,让我们更仔细地看看这些隐藏的数据。为此,您将再次使用一些命令行工具来连接并查询数据库。例如,尝试对之前创建的文件运行find()命令:
$ mongo test
MongoDB shell version: 2.5.1-pre
connecting to: test
> db.fs.files.find()
{ "_id" : ObjectId("51cb61b26487b3d8ce7af440"), "filename" : "/tmp/dictionary", "chunkSize" : 262144, "uploadDate" : ISODate("2013-06-26T21:48:34.621Z"), "md5" : "40c0825855792bd20e8a2d515fe9c3e3", "length" : 4953699 }
>
输出应该看起来很熟悉——毕竟,它是您在本章前面看到的相同数据。现在您可以看到由mongofiles打印的信息来自于fs.files集合中的文件条目。
接下来我们来看看chunks集合(要加滤镜;否则,它也会显示所有原始的二进制数据):
$ mongo test
MongoDB shell version: 2.5.1-pre
connecting to: test
> db.fs.chunks.find({},{"data":0});
{ "_id" : ObjectId("51cb61b29b2daad9857ca205"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 4 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca206"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 5 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca207"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 6 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca208"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 7 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca209"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 8 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca20a"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 9 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca20b"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 10 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca20c"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 11 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca20d"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 12 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca20e"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 13 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca20f"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 14 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca210"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 15 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca211"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 16 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca212"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 17 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca201"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 0 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca202"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 1 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca203"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 2 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca204"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 3 }
{ "_id" : ObjectId("51cb61b29b2daad9857ca213"), "files_id" : ObjectId("51cb61b26487b3d8ce7af440"), "n" : 18 }>
您可能想知道为什么这里的输出有这么多条目。如前所述,GridFS 只是一个规范。也就是说,它使用 MongoDB 已经提供的内容。当我们测试这本书的命令时,字典文件被添加了几次。后来我们清空了fs.files集合,这个文件就被删除了。你可以亲眼看看接下来发生了什么!从一个集合中删除一些文档的事实与另一个集合中发生的事情没有关系。记住:MongoDB 不会以任何特殊的方式对待这些文档或集合。如果该文件已经通过驱动程序或mongofiles工具被正确删除,该工具也会清理掉chunks集合。
Warning
直接访问文档和集合是一个强大的特性,但是您需要小心。这个功能也使得同时拍摄自己的双脚变得容易得多。如果您决定手动编辑这些文档和集合,请确保您知道自己在做什么,并且执行大量的测试。另外,请记住,MongoDB 驱动程序中的 GridFS 支持不会知道您所做的任何定制。
使用搜索命令
接下来,我们仔细看看 MongoDB 的search命令。到目前为止,数据库中只有一个文件,这极大地限制了您可以进行的搜索类型!所以再补充点别的。以下代码片段将词典复制到另一个文件,然后导入该文件:
$ cp /tmp/dictionary /tmp/hello_world
$ mongofiles put /tmp/hello_world
connected to: 127.0.0.1
added file: { _id: ObjectId('51cb63d167961ebc919edbd5'), filename: "/tmp/hello_world", chunkSize: 262144, uploadDate: new Date(1372283858021), md5: "40c0825855792bd20e8a2d515fe9c3e3", length: 4953699 }done!
root@core2:∼# mongofiles list
connected to: 127.0.0.1
/tmp/dictionary 4953699
/tmp/hello_world 4953699
$
第一行复制文件,第二行将其导入 MongoDB。和前面的例子一样,put命令打印出 MongoDB 创建的新文档。接下来,您可以运行mongofiles命令list来检查文件是否被正确存储。如果这样做,您可以看到集合中现在有两个文件;不出所料,这两个文件大小相同。
search命令完全按照您的预期工作。你只需告诉mongofiles你在找什么,它就会帮你找到,如下例所示:
$ mongofiles search hello
connected to: 127.0.0.1
/tmp/hello_world 4953699
$ mongofiles search dict
connected to: 127.0.0.1
/tmp/dictionary 4953699
$
再说一次,这里没有什么太令人兴奋的事情发生。然而,有一点很重要,值得注意。MongoDB 可以根据您的需要简单或复杂。mongofiles工具仅供参考使用,包含非常基础的调试。好消息是:MongoDB 使得对文件执行简单的搜索变得很容易。更好的消息是:如果你想写一些非常复杂的搜索,MongoDB 也会支持你。
删除
mongofiles命令delete不需要太多解释,但它确实值得一个大警告。此命令根据文件名删除文件。因此,如果您有多个同名文件,此命令将删除所有文件。下面的代码片段显示了如何使用delete命令:
$ mongofiles delete /tmp/hello_world
connected to: 127.0.0.1
$ mongofiles list
connected to: 127.0.0.1
/tmp/dictionary 4953699
$
Note
许多人评论过这个问题,删除多个同名文件不是问题,因为没有应用会有重名。这根本不是真的;而且在很多情况下,强制使用唯一的名称甚至没有意义。例如,如果你的应用允许用户上传照片到他们的个人资料,你收到的一半文件很可能会被称为photo.jpg或me.png。
当然,如果你不太可能使用mongofiles来管理你的实时数据——事实上没有人期望它会被这样使用——那么你只需要在删除数据时小心。
从 MongoDB 检索文件
到目前为止,您实际上还没有从 MongoDB 中取出任何文件。任何数据库最重要的特征是,一旦数据被输入,它就能让你找到并检索数据。下面的代码片段使用mongofiles命令get从 MongoDB 中检索一个文件:
$ mongofiles get /tmp/dictionary
connected to: 127.0.0.1
done write to: /tmp/dictionary
$
这个例子包括一个故意的错误。因为它指定了您想要检索的文件的完整名称和路径(根据需要),mongofiles将数据写入具有相同名称和路径的文件。实际上,这会覆盖原来的字典文件!这并不是很大的损失,因为它被同一个文件覆盖了——而且字典文件首先只是一个临时副本。然而,如果你不小心抹掉了两周的工作,这种行为会给你一个相当严重的打击。相信我们,你不会知道你所有的工作去了哪里,直到活动结束后的某个时候!当使用delete命令时,你需要小心使用get命令。
总结 mongofiles
mongofiles实用程序是快速查看数据库内容的有用工具。如果你写了一些软件,你怀疑它可能有问题,那么你可以使用mongofiles来再次检查发生了什么。
这是一个非常简单的实现,所以不需要任何复杂的逻辑来完成手头的任务。你是否会在生产环境中使用mongofiles是个人喜好的问题。这不完全是瑞士军刀。但是,它确实提供了一组有用的命令,如果您的应用开始出现问题,您会非常感激。简而言之,你应该熟悉这个工具,因为有一天它可能正是你解决一个棘手问题所需要的工具。
利用 Python 的力量
至此,您已经对 GridFS 的工作原理有了一个很好的了解。接下来,您将学习如何从 Python 访问 GridFS。第 2 章讲述了如何安装 PyMongo 如果你对这些例子有任何疑问,请回头参考第 2 章并确保所有东西都安装正确。
如果你已经按照本章前面的例子做了,你现在应该在 GridFS 中有一个文件了。您还会记得,该文件是一个字典文件,因此它包含一个单词列表。在本节中,您将学习如何编写一个简单的 Python 脚本来打印出字典文件中的所有单词。当然,简化原始文件会更简单、更有效——但是这有什么意思呢?
首先启动 Python:
Python 2.6.6 (r266:84292, Oct 12 2012, 14:23:48)
[GCC 4.4.6 20120305 (Red Hat 4.4.6-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.>>>
Python 的标准驱动程序叫做 PyMongo,由 Mike Dirolf 编写。因为 PyMongo 驱动程序直接由 MongoDB,Inc .提供支持,MongoDB 的发布公司,您可以放心,它会定期更新和维护。因此,让我们继续导入库。您应该会看到如下所示的内容:
>>> from pymongo import Connection
>>> import gridfs
>>>
如果 PyMongo 安装不正确,您将会看到类似如下的错误:
>>> import gridfs
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named gridfs
>>>
如果您看到后一条消息,可能是安装过程中遗漏了什么。在这种情况下,请返回到第 2 章并按照说明重新安装 PyMongo。
连接到数据库
在从数据库中检索信息之前,必须首先建立与数据库的连接。当您在本章前面使用mongofiles实用程序时,您可能注意到了对127.0.0.1的引用。这个值也称为 localhost,它代表您的计算机的回送地址。这个值只是告诉计算机自言自语的快捷方式。mongofiles提到这个 IP 地址的原因是它实际上是通过网络连接到 MongoDB 的。默认情况下,在默认的 MongoDB 端口上连接到本地机器。因为您没有更改默认设置,mongofiles可以毫不费力地找到并连接到您的数据库。
然而,在 Python 中使用 MongoDB 时,您需要连接到数据库,然后设置 GridFS。幸运的是,这很容易做到:
>>> db = Connection().test
>>> fs = gridfs.GridFS(db)
>>>
第一行打开连接并选择数据库。默认情况下,mongofiles使用test数据库;因此,您将在test中找到您的字典文件。第二行设置 GridFS 并准备使用。
获取单词
在最初的实现中,PyMongo 驱动程序使用类似文件的接口来利用 GridFS。这与你在本章前面的例子中看到的mongofiles有些不同,它们本质上更像 FTP。在 PyMongo 的原始实现中,您可以像对待普通文件一样读写数据。
这使得 PyMongo 使用起来非常像 Python,并且允许与现有脚本轻松集成。但是,在 1.6 版的驱动程序中,此行为已被更改,并且不再支持此功能。虽然非常类似于 Python,但该行为存在一些问题,使得该工具整体效率较低。
一般来说,PyMongo 驱动程序试图让 GridFS 文件看起来和感觉上像文件系统中的普通文件。一方面,这很好,因为这意味着没有学习曲线,驱动程序可以用于任何需要文件的方法。另一方面,这种方法有些局限性,不能很好地体现 GridFS 的强大。PyMongo 在 1.6 版本中的工作方式有了重要的改变,尤其是在get和put的工作方式上。
Note
PyMongo 的这个修订版与该工具的以前版本没有太大的不同,许多使用以前 API 的人发现适应修订版很容易。也就是说,迈克的改变并没有得到所有人的认可。例如,有些人发现旧 API 中基于文件的键控非常有用且易于使用。PyMongo 的修订版支持创建文件名的能力,因此缺失的行为可以在修订版中复制;然而,这样做确实需要更多的代码。
将文件放入 MongoDB
通过 PyMongo 将文件放入 GridFS 非常简单,这与使用命令行工具的方式非常相似。MongoDB 完全是关于吞吐量的,PyMongo 修订版中对 API 的更改反映了这一点。您不仅获得了更好的性能,而且这些变化还使 Python 驱动程序与其他 GridFS 实现保持一致。
让我们(再次)将字典放入 GridFS:
>>> with open("/tmp/dictionary") as dictionary:
... uid = fs.put(dictionary)
...
>>> uid
ObjectId('51cb65be2f50332093f67b98') >>>
在这个例子中,您使用了put方法来插入文件。从这个方法中获取结果很重要,因为它包含了您的文件的文档_id。PyMongo 采用了与mongofiles不同的方法,后者假设文件名是有效的键(即使你可以有重复的)。相反,PyMongo 基于它们的_id来引用文件。如果您没有捕获这些信息,那么您将无法可靠地再次找到该文件。实际上,这并不完全正确——你可以很容易地搜索一个文件——但是如果你想将这个文件链接到一个特定的用户帐户,那么你需要这个_id。
可以与put命令结合使用的两个有用参数是filename和content_type。如您所料,这些参数允许您分别设置文件的文件名和内容类型。这对于直接从磁盘加载文件很有用。然而,当您处理通过互联网接收或在内存中生成的文件时,它甚至更方便,因为在这些情况下,您可以使用类似文件的语义,但实际上不必在磁盘上创建一个真正的文件。
从 GridFS 中检索文件
最后,您现在可以返回您的数据了!此时,您已经有了自己的unique _id,所以找到文件很容易。get方法从 GridFS 中检索一个文件:
>>> new_dictionary = fs.get(uid)
就这样!前面的代码片段返回一个类似文件的对象;因此,您可以使用下面的代码片段打印字典中的所有单词:
>>> for word in new_dictionary:
... print word
现在敬畏地看着一个单词列表在屏幕上快速滚动!好吧,这不完全是火箭科学。然而,事实上这并不复杂,也不困难,这正是 GridFS 的魅力所在——它确实像宣传的那样工作,而且是以直观和容易理解的方式工作的!
删除文件
删除文件也很容易。您所要做的就是调用fs.delete()并传递文件的_id,如下例所示:
>>> fs.delete(uid)
>>> new_dictionary = fs.get(uid)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.6/site-packages/pymongo-2.5.2-py2.6-linux-x86_64.egg/gridfs/__init__.py", line 140, in get
return GridOut(self.__collection, file_id)
File "/usr/lib/python2.6/site-packages/pymongo-2.5.2-py2.6-linux-x86_64.egg/gridfs/grid_file.py", line 392, in __init__
(files, file_id))
gridfs.errors.NoFile: no file in gridfs collection Collection(Database(Connection('localhost', 27017), u'test'), u'fs.files') with _id ObjectId('51cb65be2f50332093f67b98') >>>
这些结果可能看起来有点可怕,但它们只是 PyMongo 表示找不到文件的方式。这并不奇怪,因为你刚刚删除了它!
摘要
在本章中,您对 GridFS 进行了快速浏览。您了解了什么是 GridFS,它如何与 MongoDB 结合在一起,以及如何使用它的基本语法。本章没有深入探讨 GridFS,但是在下一章,您将学习如何使用 PHP 将 GridFS 与一个真实的应用集成。现在,理解 GridFS 如何在存储文件和其他大块数据时节省时间和麻烦就足够了。
在下一章,你将开始把你所学的应用到实际中——特别是,你将学习如何建立一个功能齐全的地址簿!
六、PHP 和 MongoDB
Abstract
通过前五章,您已经学习了如何在 MongoDB shell 中执行各种操作。例如,您已经学习了如何添加、修改和删除文档。您还了解了 DBRef 和 GridFS 的工作方式,包括如何使用它们。
通过前五章,您已经学习了如何在 MongoDB shell 中执行各种操作。例如,您已经学习了如何添加、修改和删除文档。您还了解了 DBRef 和 GridFS 的工作方式,包括如何使用它们。
然而,到目前为止,您所了解的大多数事情都发生在 MongoDB shell 中。这是一个非常强大的应用,但是 MongoDB 软件还附带了大量的额外驱动程序(参见第 2 章了解更多相关信息),这些驱动程序允许您跳出 Shell,以编程方式完成许多其他类型的任务。
其中一个工具是 PHP 驱动程序,当您想使用 PHP 而不是 shell 时,它允许您扩展 PHP 安装来连接、修改和管理 MongoDB 数据库。当您需要设计一个 web 应用,或者没有访问 MongoDB shell 的权限时,这可能会很有帮助。正如本章将要演示的,您可以用 PHP 驱动程序执行的大多数操作与您可以在 MongoDB shell 中执行的功能非常相似;然而,PHP 驱动程序要求在一个数组中指定选项,而不是在两个花括号之间。尽管有相似之处,但是在使用 PHP 驱动程序时,你需要注意一些细节。这一章将带您了解在 MongoDB 中使用 PHP 的好处,以及如何克服前面提到的“无论如何”
这一章在许多方面把你带回到起点。您将从学习在 PHP 中导航数据库和使用集合开始。接下来,您将学习如何在 PHP 中插入、修改和删除帖子。您还将学习如何再次使用 GridFS 和 DBRef 然而,这一次,重点将是如何在 PHP 中使用它们,而不是这些技术背后的理论。
比较 MongoDB 和 PHP 中的文档
正如您之前所了解的,MongoDB 集合中的文档是使用类似 JSON 的格式存储的,该格式由键和值组成。这类似于 PHP 定义关联数组的方式,所以习惯这种格式应该不会太难。
例如,假设一个文档在 MongoDB shell 中如下所示:
contact = ( {
"First Name" : "Philip",
"Last Name" : "Moran",
"Address" : [
{
"Street" : "681 Hinkle Lake Road",
"Place" : "Newton",
"Postal Code" : "MA 02160",
"Country" : "USA"
}
],
"E-Mail" : [
" pm@example.com ",
" pm@office.com ",
" philip@example.com ",
" philip@office.com ",
" moran@example.com ",
" moran@office.com ",
" pmoran@example.com ",
"``pmoran@office.com
],
"Phone" : "617-546-8428",
"Age" : 60
})
当包含在 PHP 的一个数组中时,同一个文档看起来像这样:
$contact = array(
"First Name" => "Philip",
"Last Name" => "Moran",
"Address" => array(
"Street" => "681 Hinkle Lake Road",
"Place" => "Newton",
"Postal Code" => "MA 02160",
"Country" => "USA"
)
,
"E-Mail" => array(
" pm@example.com ",
" pm@office.com ",
" philip@example.com ",
" philip@office.com ",
" moran@example.com ",
" moran@office.com ",
" pmoran@example.com ",
"``pmoran@office.com
),
"Phone" => "617-546-8428",
"Age" => 60
);
这份文件的两个版本看起来很相似。明显的区别是,在 PHP 中,冒号(:)作为键/值分隔符被一个类似箭头的符号(= >)所取代。你会很快习惯这些句法上的差异。
蒙戈布班
MongoDB 的 PHP 驱动程序包含四个核心类,几个用于处理 GridFS 的类,还有几个用于表示 MongoDB 数据类型的类。核心类构成了驱动程序最重要的部分。总之,这些类允许您执行一组丰富的命令。可用的四个核心类如下:
MongoClient:向数据库发起,提供connect()、close()、listDBs()、selectDBs()、selectCollection()等数据库服务器命令。MongoDB:与数据库交互,提供createCollection()、selectCollection()、createDBRef()、getDBRef()、drop()、getGridFS()等命令。MongoCollection:与收藏互动。包括count()、find()、findOne()、insert()、remove()、save()、update()等命令。MongoCursor:与find()命令返回的结果进行交互,包括getNext()、count()、hint()、limit()、skip()、sort()等命令。
在这一章中,我们将看看所有前面的命令;毫无疑问,您将最常使用这些命令。
Note
本章将不讨论前面按类分组的命令;相反,这些命令将尽可能按照逻辑顺序进行排序。
连接和断开
让我们从研究如何使用 MongoDB 驱动程序来连接和选择数据库和集合开始。使用Mongo类建立连接,该类也用于数据库服务器命令。以下示例显示了如何在 PHP 中快速连接到数据库:
// Connect to the database
$c = new MongoClient();
// Select the database you want to connect to, e.g contacts
$c->contacts;
Mongo类还包括selectDB()函数,您可以使用它来选择数据库:
// Connect to the database
$c = new MongoClient();
// Select the database you want to connect to, e.g. contacts
$c->selectDB("contacts");
下一个示例显示了如何选择要使用的集合。与在 shell 中工作时应用的规则相同:如果您选择了一个尚不存在的集合,那么当您将数据保存到其中时,将会创建该集合。选择要连接的集合的过程类似于连接到数据库的过程;换句话说,您使用(->)语法直接指向相关的集合,如下例所示:
// Connect to the database
$c = new Mongo();
// Selecting the database ('contacts') and collection ('people') you want
// to connect to
$c->contacts->people;
selectCollection()功能还允许您选择或切换收藏,如下例所示:
// Connect to the database
$c = new Mongo();
// Selecting the database ('contacts') and collection ('people') you want
// to connect to
$c-> selectDB("contacts")->selectCollection("people");
在选择数据库或集合之前,有时需要找到所需的数据库或集合。Mongo类包括两个附加命令,用于列出可用的数据库以及可用的集合。您可以通过调用listDBs()函数并打印输出(将放在一个数组中)来获取可用数据库的列表:
// Connecting to the database
$c = new Mongo();
// Listing the available databases
print_r($c->listDBs());
同样,您可以使用listCollections()来获得数据库中可用集合的列表:
// Connecting to the database
$c = new Mongo();
// Listing the available collections within the 'contacts' database
print_r($c->contacts->listCollections());
Note
本例中使用的print_r命令是一个打印数组内容的 PHP 命令。listDBs()函数直接返回一个数组,所以该命令可以作为print_r函数的参数。
MongoClient类还包含一个close()函数,可以用来断开 PHP 会话与数据库服务器的连接。然而,通常不需要使用它,除非在不寻常的情况下,因为只要Mongo对象超出范围,驱动程序就会自动干净地关闭到数据库的连接。
有时您可能不想强行关闭连接。例如,您可能不确定连接的实际状态,或者您可能希望确保可以建立新的连接。在这种情况下,可以使用close()函数,如下例所示:
// Connecting to the database
$c = new Mongo();
// Closing the connection
$c->close();
插入数据
到目前为止,您已经看到了如何建立到数据库的连接。现在是时候学习如何将数据插入到您的集合中了。在 PHP 中这样做的过程与使用 MongoDB shell 时没有什么不同。该过程有两个步骤。首先,在变量中定义文档。其次,使用insert()函数插入它。
定义文档并不特别与 MongoDB 相关,而是创建一个存储了键和值的数组,如下例所示:
$contact = array(
"First Name" => "Philip",
"Last Name" => "Moran",
"Address" => array(
"Street" => "681 Hinkle Lake Road",
"Place" => "Newton",
"Postal Code" => "MA 02160",
"Country" => "USA"
)
,
"E-Mail" => array(
" pm@example.com ",
" pm@office.com ",
" philip@example.com ",
" philip@office.com ",
" moran@example.com ",
" moran@office.com ",
" pmoran@example.com ",
"``pmoran@office.com
),
"Phone" => "617-546-8428",
"Age" => 60
);
Warning
发送到数据库的字符串需要采用 UTF-8 格式,以防止出现异常。
一旦您将数据正确地赋给了一个变量——在本例中称为$contact——您就可以使用insert()函数将其插入到MongoCollection类中:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people'
$collection = $c->contacts->people;
// Insert the document '$contact' into the people collection '$collection'
$collection->insert($contact);
insert()函数有五个选项,在一个数组中指定:fsync, j, w, wtimeout和timeout。fsync选项可以设置为TRUE或FALSE;FALSE是该选项的默认值。如果设置为TRUE,fsync在指示插入成功之前,强制将数据写入硬盘。该选项将覆盖选项w的任何设置,将其设置为 0。j选项可以设置为TRUE或FALSE,默认为FALSE。如果设置,j选项将在指示插入成功之前强制将数据写入日志。如果您不熟悉日志记录,可以把它想象成一个日志文件,在数据最终写入磁盘之前,记录对数据所做的更改。这确保了如果mongod意外停止,它将能够恢复写入日志的更改,从而防止您的数据进入不一致状态。
w选项可用于确认或不确认写操作(使该选项也适用于remove()和update()操作)。如果w置 0,写操作将不被确认;将其设置为 1,写操作将被(主)服务器确认。当使用副本集时,w也可以设置为n,确保主服务器在成功复制到n节点时确认写操作。w也可以设置为'majority'—一个保留字符串—确保大多数副本集将确认写入,或者设置为一个特定的标记,确保那些标记的节点将确认写入。对于此选项,默认设置也是 1。wtimeout选项可用于指定服务器等待接收确认的时间(以毫秒为单位)。默认情况下,该选项设置为 10000。最后,timeout选项允许您指定客户机需要等待数据库响应多长时间(毫秒)。
以下示例说明了如何使用w and wtimeout选项插入数据:
// Define another contact
$contact = array(
"Fir't Name" => "Victoria",
"Last Name" => "Wood",
"Address" => array(
"Street" => "50 Ash lane",
"Place" => "Ystradgynlais",
"Postal Code" => "SA9 6XS",
"Country" => "UK"
)
,
"E-Mail" => array(
" vw@example.com ",
"``vw@office.com
),
"Phone" => "078-8727-8049",
"Age" => 28
);
// Connect to the database
$c = new MongoClient();
// Select the collection 'people'
$collection = $c->contacts->people;
// Specify the w and wtimeout options
$options = array("w" => 1, "wtimeout" => 5000);
// Insert the document '$contact' into the people collection '$collection'
$collection->insert($contact,$options);
这就是用 PHP 驱动程序将数据插入数据库的全部内容。在大多数情况下,您可能会定义包含数据的数组,而不是将数据注入数组。
列出您的数据
通常,您将使用find()函数来查询数据。它使用一个参数来指定您的搜索条件;一旦指定了标准,就执行find()来获得结果。默认情况下,find()函数只是返回集合中的所有文档。这类似于在第 4 章中讨论的 shell 示例。然而,大多数时候,你并不想这样做。相反,您会希望定义特定的信息来返回结果。接下来的部分将涵盖常用的选项和参数,您可以使用find()功能来过滤您的结果。
返回单个文档
列出单个文档很容易:只需执行没有指定任何参数的findOne()函数,就可以获取它在集合中找到的第一个文档。findOne函数将返回的信息存储在一个数组中,让您再次打印出来,如下例所示:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Find the very first document within the collection, and print it out
// using print_r
print_r($collection->findOne());
如前所述,在集合中列出单个文档很容易:您需要做的就是定义findOne()函数本身。当然,您可以使用带有附加过滤器的findOne()函数。例如,如果你知道你要找的人的姓,你可以在findOne()函数中指定一个选项:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Define the last name of the person in the $lastname variable
$lastname = array("Last Name" => "Moran");
// Find the very first person in the collection with the last name "Moran"
print_r($collection->findOne($lastname));
当然,过滤数据还有更多的选择;在本章的后面,您将了解更多关于这些附加选项的信息。让我们从查看使用print_r()命令返回的一些示例输出开始(为了使代码更容易阅读,该示例添加了一些换行符):
Array (
[_id] => MongoId Object ( )
[First Name] => Philip
[Last Name] => Moran
[Address] => Array (
[Street] => 681 Hinkle Lake Road
[Place] => Newton
[Postal Code] => MA 02160
[Country] => USA
)
[E-Mail] => Array (
[0] => pm@example.com
[1] => pm@office.com
[2] => philip@example.com
[3] => philip@office.com
[4] => moran@example.com
[5] => moran@office.com
[6] => pmoran@example.com
[7] => pmoran@office.com
)
[Phone] => 617-546-8428
[Age] => 60
)
列出所有文档
虽然您可以使用findOne()函数来列出单个文档,但是您将使用find()函数来处理几乎所有其他事情。请不要误会:通过限制你的结果,可以找到带有find()功能的单个文档;但是如果您不确定要返回的文档数量,或者如果您期望不止一个文档,那么find()函数是您的好朋友。
如前几章所述,find()函数有很多很多选项,您可以使用这些选项来过滤您的结果,以适应您能想象到的任何情况。我们将从几个简单的例子开始,并以此为基础进行构建。
首先,让我们看看如何使用 PHP 和find()函数显示某个集合中的所有文档。在打印多个文档时,唯一需要注意的是每个文档都以数组的形式返回,并且每个数组都需要单独打印。您可以使用 PHP 的while()函数来完成这项工作。如前所述,您需要指示函数在处理下一个文档之前打印每个文档。getNext()命令从 MongoDB 获取光标中的下一个文档;该命令有效地返回光标中的下一个对象,并向前移动光标。以下代码片段列出了集合中的所有文档:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Execute the query and store it under the $cursor variable
$cursor = $collection->find();
// For each document it finds within the collection, print the contents
while ($document = $cursor->getNext())
{
print_r($document);
}
Note
您可以用几种不同的方式实现前面示例的语法。例如,执行前面的命令的一种更快的方式如下:$cursor = $c->contacts->people->find()。然而,为了清楚起见,像这样的代码示例将在本章中分成两行,为注释留出更多空间。
在这个阶段,结果输出仍然只显示两个数组,假设您已经添加了本章前面描述的文档(没有其他内容)。如果您要添加更多的文档,那么每个文档都将以自己的数组打印。当然,这看起来不太好;然而,只要增加一点代码,就没什么不能解决的。
使用查询运算符
无论您在 MongoDB shell 中能做什么,您也可以使用 PHP 驱动程序来完成。正如您在前面的章节中看到的,shell 包含了几十个过滤结果的选项。例如,可以使用点符号;对结果进行排序或限制;跳过、计数或分组一些项目。或者甚至使用正则表达式等等。下面几节将带您了解如何在 PHP 驱动程序中使用这些选项。
查询特定信息
您可能还记得第 4 章中的内容,您可以使用点符号来查询文档中嵌入对象的特定信息。例如,如果您想查找某个您知道部分地址详细信息的联系人,您可以使用点符号来查找,如下例所示:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Use dot notation to search for a document in which the place
// is set to "Newton"
$address = array("Address.Place" => "Newton");
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($address);
// For each document it finds within the collection, print the ID
// and its contents
while ($document = $cursor->getNext())
{
print_r($document);
}
以类似的方式,您可以通过指定文档数组中的一项(如电子邮件地址)来搜索该数组中的信息。因为电子邮件地址(通常)是唯一的,所以在这个例子中使用findOne()函数就足够了:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Define the e-mail address you want to search for under $email
$email = array("E-Mail" => "``vw@example.com
// Find the very first person in the collection matching the e-mail address
print_r($collection->findOne($email));
正如所料,这个例子返回第一个匹配电子邮件地址vw@example.com的文档,在这个例子中是维多利亚·伍德的地址。文档以数组的形式返回:
Array (
[_id] => MongoId Object ( )
[First Name] => Victoria
[Last Name] => Wood
[Address] => Array (
[Street] => 50 Ash lane
[Place] => Ystradgynlais
[Postal Code] => SA9 6XS
[Country] => UK
)
[E-Mail] => Array (
[0] => vw@example.com
[1] => vw@office.com
)
[Phone] => 078-8727-8049
[Age] => 28
)
排序、限制和跳过项目
MongoCursor类提供了sort()、limit()和skip()函数,分别允许您对结果进行排序、限制返回结果的总数以及跳过特定数量的结果。让我们使用 PHP 驱动程序来检查每个函数以及它是如何使用的。
PHP 的sort()函数将一个数组作为参数。在该数组中,您可以指定对文档进行排序所依据的字段。与使用 shell 时一样,使用值1对结果进行升序排序,使用-1对结果进行降序排序。请注意,您是在现有光标上执行这些功能的——也就是说,针对之前执行的find()命令的结果。
以下示例根据联系人的年龄对其进行升序排序:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Execute the query and store it under the $cursor variable
$cursor = $collection->find();
// Use the sort command to sort all results in $cursor, based on their age
$cursor->sort(array('Age' => 1));
// Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
您在实际光标上执行limit()函数;这总共需要一个参数,它指定了您希望返回的结果的数量。limit()命令返回它在集合中找到的符合搜索条件的前 n 个项目。下面的例子只返回一个文档(当然,您可以使用findOne()函数来代替,但是limit()可以完成这项工作):
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Execute the query and store it under the $cursor variable
$cursor = $collection->find();
// Use the limit function to limit the number of results to 1
$cursor->limit(1);
//Print the result
while($document = $cursor->getNext())
{
print_r($document);
}
最后,您可以使用skip()函数跳过符合您的标准的前 n 个结果。该函数也适用于光标:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Execute the query and store it under the $cursor variable
$cursor = $collection->find();
// Use the skip function to skip the first result found
$cursor->skip(1);
// Print the result
while($document = $cursor->getNext())
{
print_r($document);
}
统计匹配结果的数量
您可以使用 PHP 的count()函数来计算符合您的标准的文档数,并返回数组中的项目数。该函数是MongoCursor类的一部分,因此对光标进行操作。以下示例显示如何获取居住在美国的人在集合中的联系人计数:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'$collection = $c->contacts->people;
// Specify the search parameters
$country = array("Address.Country" => "USA");
// Execute the query and store under the $cursor variable for further processing
$cursor = $collection->find($country);
// Count the results and return the value
print_r($cursor->count());
该查询返回一个结果。这种计数对于各种操作都很有用,无论是计算评论数、注册用户总数还是其他任何操作。
使用聚合框架对数据进行分组
聚合框架很容易成为 MongoDB 内置的更强大的特性之一,因为它允许您计算聚合值,而无需使用通常过于复杂的 Map/Reduce 功能。框架包含的最有用的管道操作符之一是$group操作符,它可以粗略地与 SQL 的 GROUP BY 功能相比较。该运算符允许您基于一组文档计算聚合值。比如聚合函数$max可以用来寻找并返回一个组的最高值;$min函数查找并返回最小值,而$sum函数计算给定值出现的总次数。
假设您想要获得您的集合中所有联系人的列表,按他们居住的国家分组。聚合框架让您可以轻松做到这一点。让我们来看一个例子:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Execute the query and store it under the $result variable
$result = $collection->aggregate(array(
'$group' => array(
'_id' => '$Address.Country',
'total' => array('$sum' => 1)
)
));
// Count the results and return the value
print_r($result);
如您所见,aggregate函数接受一个(或多个)带有管道操作符的数组(在本例中是$group操作符)。在这里,您可以指定如何返回结果输出,以及要执行的任何可选聚合函数:在本例中是$sum函数。在这个例子中,为每个找到的唯一国家返回一个唯一的文档,由文档的_id字段表示。接下来,使用$sum函数汇总每个国家的总计数,并使用total字段返回。注意,$sum函数是由一个数组表示的,给定值 1 是因为我们希望每一个匹配都将总数增加 1。
您可能想知道最终的输出会是什么样子。下面是一个输出示例,假设有两个联系人生活在英国,一个生活在美国:
Array (
[result] => Array (
[0] => Array (
[_id] => UK [total] => 2
)
[1] => Array (
[_id] => USA [total] => 1
)
)
[ok] => 1
)
这个例子很简单,但是聚合框架确实非常强大,当我们在第 8 章更深入地研究它时,你会看到这一点。
使用提示指定索引
您使用 PHP 的hint()函数来指定查询数据时应该使用哪个索引;这样做可以帮助您提高查询性能。例如,假设您的集合中有数千个联系人,您通常根据姓氏来搜索一个人。在这种情况下,建议您在集合中的Last Name键上创建一个索引。
Note
如果没有首先创建索引,接下来显示的hint()示例将不会返回任何内容。
要使用hint()函数,您必须将其应用于光标,如下例所示:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Execute the query and store it under the $cursor variable
$cursor = $collection->find(array("Last Name" => "Moran"));
// Use the hint function to specify which index to use
$cursor->hint(array("Last Name" => -1));
//Print the result
while($document = $cursor->getNext())
{
print_r($document);
}
Note
有关如何创建索引的更多详细信息,请参见第 4 章。也可以使用 PHP 驱动程序的ensureIndex()函数来创建一个索引,如前所述。
用条件运算符细化查询
您可以使用条件运算符来优化查询。PHP 附带了一组很好的默认条件操作符,比如<(小于)、>(大于)、<=(小于或等于)和>=(大于或等于)。现在是坏消息:你不能在 PHP 驱动中使用这些操作符。相反,您将需要使用 MongoDB 版本的这些操作符。幸运的是,MongoDB 本身带有大量的条件操作符(您可以在第 4 章的中找到关于这些操作符的更多信息)。当通过 PHP 查询数据时,可以使用所有这些操作符,通过find()函数传递它们。
虽然可以在 PHP 驱动程序中使用所有这些操作符,但是必须使用特定的语法;也就是说,您必须将它们放在一个数组中,并将该数组传递给find()函数。下面几节将向您介绍如何使用几种常用的运算符。
使用\(lt、\)gt、\(lte 和\)gte 运算符
MongoDB 的$lt、$gt、$lte和$gte操作符允许您分别执行与<、>、<=和>=操作符相同的操作。当您想要搜索存储整数值的文档时,这些运算符非常有用。
您可以使用$lt(小于)运算符来查找整数值小于 n 的任何类型的数据,如下例所示:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('Age' => array('$lt' => 30));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
结果输出显示当前文档中只有一个结果:维多利亚·伍德的联系信息,他刚好不到 30 岁:
Array (
[_id] => MongoId Object ( )
[First Name] => Victoria
[Last Name] => Wood
Address] => Array (
[Street] => 50 Ash lane
[Place] => Ystradgynlais
[Postal Code] => SA9 6XS
[Country] => UK
)
[E-Mail] => Array (
[0] => vw@example.com
[1] => vw@office.com
)
[Phone] => 078-8727-8049
[Age] => 28
)
同样,您可以使用$gt操作符来查找任何年龄超过 30 岁的联系人。下面的例子通过将变量$lt改为$gt(大于)来实现:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('Age' => array('$gt' => 30));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
这将返回 Philip Moran 的文档,因为他已经超过 30 岁了:
Array (
[_id] => MongoId Object ( )
[First Name] => Philip
[Last Name] => Moran
[Address] => Array (
[Street] => 681 Hinkle Lake Road
[Place] => Newton
[Postal Code] => MA 02160
[Country] => USA
)
[E-Mail] => Array (
[0] => pm40%example.com
[1] => pm40%office.com
[2] => philip40%example.com
[3] => philip40%office.com
[4] => moran40%example.com
[5] => moran40%office.com
[6] => pmoran40%example.com
[7] => pmoran@office.com
)
[Phone] => 617-546-8428
[Age] => 60
)
您可以使用$lte操作符来指定该值必须完全匹配或者小于指定的值。记住:$lt会找到任何小于 30 岁的人,但不会找到正好 30 岁的人。对于$gte操作符也是如此,它查找任何大于或等于指定整数的值。现在我们来看两个例子。
第一个示例将集合中的两个项目都返回到屏幕上:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('Age' => array('$lte' => 60));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
第二个示例将只显示一个文档,因为该集合只包含一个 60 岁或以上的联系人:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('Age' => array('$gte' => 60));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
查找与值不匹配的文档
您可以使用$ne(不等于)运算符来查找任何与$ne运算符中指定的值不匹配的文档。这个操作符的语法很简单。下一个示例将显示年龄不等于 28 岁的任何联系人:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('Age' => array('$ne' => 28));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
匹配多个值中的任何一个
$in操作符允许您搜索与添加到数组中的几个可能值相匹配的文档,如下例所示:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('Address.Country' => array('$in' => array("USA","UK")));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
结果输出将显示您添加的任何人的任何联系信息,无论此人居住在美国还是英国。注意,可能性列表实际上是添加在一个数组中的;它不能以“就这样”的形式键入。
用$all 匹配查询中的所有标准
像$in操作符一样,$all操作符允许您比较一个附加数组中的多个值。不同之处在于,$all操作符要求数组中的所有项都匹配一个文档,然后它才返回任何结果。以下示例显示了如何进行这样的查询:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('E-Mail' => array('$all' => array("``vw@example.com``","``vw@office.com
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
使用$或搜索多个表达式
您可以使用$or操作符来指定一个文档可以包含的多个表达式,以返回一个匹配。这两个操作符的区别在于,$in操作符不允许你同时指定键和值,而$or操作符允许。您可以将$or操作符与任何其他键/值组合结合使用。我们来看两个例子。
第一个示例搜索并返回任何包含整数值为28的Age键或值为USA的Address.Country键的文档:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('$or' => array(
array("Age" => 28),
array("Address.Country" => "USA")
) );
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
第二个示例搜索并返回任何将Address.Country键设置为USA(强制),以及将键/值设置为"Last Name" : "Moran"或"E-Mail" : " vw@example.com "的文档:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array(
"Address.Country" => "USA",
'$or' => array(
array("Last Name" => "Moran"),
array("E-Mail" => "``vw40%example.com
)
);
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
$or操作符允许您一次进行两个搜索,然后合并结果输出,即使这两个搜索没有任何共同点。
使用$slice 检索指定数量的项目
您可以使用$slice投影操作符从文档的数组中检索指定数量的项目。该功能类似于本章前面详述的skip()和limit()功能。不同之处在于,skip()和limit()函数处理整个文档,而$slice操作符允许您处理一个数组而不是单个文档。
投影操作符是限制每页项目数量的一个很好的方法(这就是通常所说的分页)。下一个示例显示了如何限制从前面指定的联系人之一(Philip Moran)返回的电子邮件地址的数量;在这种情况下,您只需返回前三个电子邮件地址:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify our search operator
$query = array("Last Name" => "Moran");
// Create a new object from an array using the $slice operator
$cond = (object)array('E-Mail' => array('$slice' => 3));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($query, $cond);
// For each document it finds within the collection, print the contents
while ($document = $cursor->getNext())
{
print_r($document);
}
同样,通过将整数设为负数,您只能获得列表中最后三个电子邮件地址,如下例所示:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify our search operator
$query = array("Last Name" => "Moran");
// Specify the conditional operator
$cond = (object)array('E-Mail' => array('$slice' => -3));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($query, $cond);
// For each document it finds within the collection, print the contents
while ($document = $cursor->getNext())
{
print_r($document);
}
或者,您可以跳过前两个条目,将结果限制为三个:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify our search operator
$query = array("Last Name" => "Moran");
// Specify the conditional operator
$cond = (object)array('E-Mail' => array('$slice' => array(2, 3)));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($query, $cond);
// For each document it finds within the collection, print the contents
while ($document = $cursor->getNext())
{
print_r($document);
}
$slice操作符是限制数组中项目数量的好方法;在使用 MongoDB 驱动程序和 PHP 编程时,您肯定希望记住这个操作符。
确定字段是否有值
您可以使用$exists操作符返回一个基于字段是否有值的结果(不管这个字段的值是多少)。这听起来可能不合逻辑,但实际上非常方便。例如,您可以搜索尚未设置Age字段的联系人;或者,您可以搜索有街道名称的联系人。
以下示例返回没有设置Age字段的所有联系人:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array('Age' => array('$exists' => false));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
类似地,下一个示例返回设置了Street字段的所有联系人:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the conditional operator
$cond = array("Address.Street" => array('$exists' => true));
// Execute the query and store it under the $cursor variable
$cursor = $collection->find($cond);
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
正则表达式
正则表达式很整洁。你可以用它们做任何事情(也许除了煮咖啡);在搜索数据时,它们可以极大地简化您的生活。PHP 驱动自带了自己的正则表达式类:MongoRegex类。您可以使用这个类来创建正则表达式,然后使用它们来查找数据。
MongoRegex类知道六个正则表达式标志,您可以用它们来查询您的数据。你可能已经熟悉其中的一些:
i:触发不区分大小写。m:搜索跨多行(换行符)的内容。x:允许您的搜索包含#条评论。l:指定语言环境。s:又称 dotall,“”可以指定为匹配所有内容,包括新行。u:匹配 Unicode。
现在让我们仔细看看如何在 PHP 中使用正则表达式来搜索集合中的数据。显然,这最好用一个简单的例子来说明。
例如,假设您想搜索一个信息很少的联系人。例如,你可能会模糊地回忆起这个人居住的地方,并且在中间的某个地方包含了类似 stradgynl 的东西。正则表达式为您提供了一种简单而优雅的方法来搜索这样的人:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the regular expression
$regex = new MongoRegex("/stradgynl/i");
// Execute the query and store it under the $cursor variable
$cursor = $collection->find(array("Address.Place" => $regex));
//Print the results
while($document = $cursor->getNext())
{
print_r($document);
}
当创建一个 PHP 应用时,您通常希望搜索特定的数据。在前面的例子中,您可能会用一个$_POST变量替换文本(本例中为"stradgynl")。
用 PHP 修改数据
如果我们生活在一个所有数据都保持静态、人类从不犯任何打字错误的世界,我们就永远不需要更新我们的文档。但是这个世界比这要灵活一点,有时候我们会犯一些我们想要改正的错误。
对于这种情况,您可以在 MongoDB 中使用一组修饰符函数来更新(并因此更改)您现有的数据。你可以用几种方法做到这一点。例如,您可以使用update()函数更新现有信息,然后使用save()函数保存您的更改。接下来的几节将研究这些和其他修饰运算符,并说明如何有效地使用它们。
通过 update()更新
正如在第 4 章中所详述的,您使用update()功能来执行大多数文档更新。与 MongoDB shell 中的update()版本一样,PHP 驱动程序附带的update()函数允许您使用各种修改符操作符来快速轻松地更新您的文档。PHP 版本的update()函数的操作几乎相同;然而,成功地使用 PHP 版本需要一种非常不同的方法。下一节将向您介绍如何在 PHP 中成功使用该函数。
PHP 的update()函数至少有两个参数:第一个描述要更新的对象,第二个描述要更新匹配记录的对象。此外,您可以为扩展的选项集指定第三个参数。
options参数提供了七个额外的标志,可以与update()函数一起使用;以下列表解释了它们是什么以及如何使用它们:
upsert:如果设置为true,如果搜索标准不匹配,该布尔选项将创建一个新文档。multiple:如果设置为true,该布尔选项将更新所有符合搜索条件的文档。fsync:如果设置为true,该布尔选项会在返回成功之前将数据同步到磁盘。如果该选项被设置为true,则意味着w被设置为0,即使它没有被设置。默认为 false。w:如果设置为 0,更新操作将不被确认。使用副本集时,w也可以设置为 n,确保主服务器在成功复制到 n 个节点时确认更新操作。也可以设置为'majority'—一个保留字符串,以确保大多数副本节点将确认更新,或者设置为特定的标签,以确保那些被标记的节点将确认更新。该选项默认为 1,表示确认更新操作。j:如果设置为true,该布尔选项将在指示更新成功之前强制将数据写入日志。默认为false。wtimeout:用于指定服务器等待接收确认的时间(毫秒)。默认为 10000。timeout:用于指定客户端需要等待数据库响应的时间(毫秒)。
现在让我们来看一个普通的例子,它在不使用任何修饰运算符的情况下将维多利亚·伍德的名字改为“Vicky”(稍后将讨论这些运算符):
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Last Name" => "Wood");
// Specify the information to be changed
$update = array(
"First Name" => "Vicky",
"Last Name" => "Wood",
"Address" => array(
"Street" => "50 Ash lane",
"Place" => "Ystradgynlais",
"Postal Code" => "SA9 6XS",
"Country" => "UK"
)
,
"E-Mail" => array(
" vw40%example.com ",
"``vw40%office.com
),
"Phone" => "078-8727-8049",
"Age" => 28
);
// Options
$options = array("upsert" => true);
// Perform the update
$collection->update($criteria,$update,$options);
// Show the result
print_r($collection->findOne($criteria));
结果输出如下所示:
Array (
[_id] => MongoId Object ()
[First Name] => Vicky
[Last Name] => Wood
[Address] => Array (
[Street] => 50 Ash lane
[Place] => Ystradgynlais
[Postal Code] => SA9 6XS
[Country] => UK
)
[E-Mail] => Array (
[0] => vw40%example.com
[1] => vw40%office.com
)
[Phone] => 078-8727-8049
[Age] => 28
)
仅仅改变一个价值观就要做很多工作——这并不完全是你想要谋生的方式。然而,如果不使用 PHP 的修饰符操作符,这恰恰是您必须要做的。现在让我们来看看如何在 PHP 中使用这些操作符来使生活变得更简单,消耗更少的时间。
Warning
如果在应用更改时没有指定任何条件运算符,匹配文档中的数据将被数组中的信息替换。一般来说,如果你只想改变一个字段,最好使用$set。
使用更新运算符节省时间
更新操作将为您节省大量的输入。你可能会同意,前面的例子是不可行的。幸运的是,PHP 驱动程序包含了大约六个更新操作符,用于快速修改数据,而无需麻烦地将数据全部写出来。每个操作符的用途将再次简要总结,尽管此时您可能已经熟悉了其中的大部分(您可以在第 4 章的中找到本节讨论的所有更新操作符的更多信息)。然而,在 PHP 中使用它们的方式有很大的不同,与它们相关的选项也是如此。我们将查看每个操作符的例子,尤其是为了让您熟悉它们在 PHP 中的语法。
Note
后面的更新操作符都不会包含 PHP 代码来检查所做的更改;相反,下面的例子只是应用了这些变化。建议您在 PHP 代码旁边启动 MongoDB shell,这样您就可以执行搜索并确认所需的更改已经应用。或者,您可以编写额外的 PHP 代码来执行这些检查。
用$inc 增加特定键的值
$inc操作符允许您将特定键的值增加 n,假设该键存在。如果该键不存在,将会创建一个。以下示例将 40 岁以下的每个人的年龄增加三岁:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Search for anyone that's younger than 40
$criteria = array("Age" => array('$lt' => 40));
// Use $inc to increase their age by 3 years
$update = array('$inc' => array('Age' => 3));
// Options
$options = array("upsert" => true);
// Perform the update
$collection->update($criteria,$update,$options);
用$set 更改键值
$set操作符允许您在忽略任何其他字段的同时更改键值。如前所述,在前面的例子中,这是将 Victoria 的名字更新为"Vicky"的更好的选择。下面的例子展示了如何使用$set操作符将联系人的名字改为"Vicky":
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Last Name" => "Wood");
// Specify the information to be changed
$update = array('$set' => array("First Name" => "Vicky"));
// Options
$options = array("upsert" => true);
// Perform the update
$collection->update($criteria,$update,$options);
您还可以使用$set为匹配您的查询的每个匹配项添加一个字段:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria using regular expressions
$criteria = array("E-Mail" => new MongoRegex("``/40%office.com/i
// Add "Category => Work" into every occurrence found
$update = array('$set' => array('Category' => 'Work'));
// Options
$options = array('upsert' => true, 'multi' => true);
// Perform the upsert via save()
$collection->update($criteria,$update,$options);
删除未设置$的字段
$unset操作符的工作方式类似于$set操作符。不同之处在于,$unset让您从文档中删除给定的字段。例如,以下示例从维多利亚·伍德的联系人信息中删除了Phone字段及其相关数据:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Last Name" => "Wood");
// Specify the information to be removed
$update = array('$unset' => array("Phone" => 1));
// Perform the update
$collection->update($criteria,$update);
用$rename 重命名字段
$rename操作符可用于重命名字段。当你不小心打错了或者只是想把它的名字改成一个更准确的名字时,这很有帮助。操作符将在每个文档及其底层数组和子文档中搜索给定的字段名。
Warning
使用该运算符时要小心。如果文档已经包含具有新名称的字段,该字段将被删除,之后旧的字段名将被重命名为指定的新名称。
让我们看一个例子,对于 Vicky Wood 来说,First Name和Last Name字段将分别被重命名为Given Name和Family Name:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Last Name" => "Wood");
// Specify the information to be changed
$update = array('$rename' => array("First Name" => "Given Name", "Last Name" => "Family Name"));
// Perform the update
$collection->update($criteria,$update);
使用$setOnInsert 在 Upsert 期间更改键值
PHP 的$setOnInsert操作符仅在 update 函数在使用 upsert 操作符时执行 insert 操作的情况下才可用于指定特定值。起初这可能听起来有点混乱,但是您可以将这个操作符看作一个条件语句,它只在upsert插入文档时设置给定值,而不是更新文档。让我们看一个例子来阐明这是如何工作的。首先,我们将执行一个匹配现有文档的 upsert,从而忽略指定的$setOnInsert标准:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wood");
// Specify the information to be set on upsert-inserts only
$update = array('$setOnInsert' => array("Country" => "Unknown"));
// Specify the upsert options
$options = array("upsert" => true);
// Perform the update
$collection->update($criteria,$update,$options);
接下来,让我们看一个例子,当文档尚不存在时,upsert 执行插入。在这里,您会发现给出的$setOnInsert标准将被成功应用:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wallace");
// Specify the information to be set on upsert-inserts only
$update = array('$setOnInsert' => array("Country" => "Unknown"));
// Specify the upsert options
$options = array("upsert" => true);
// Perform the update
$collection->update($criteria,$update,$options);
这段代码将搜索任何将Family Name-字段(记得我们之前将其重命名)设置为"Wallace"的文档。如果找不到,将进行一次 upsert,结果是Country字段将被设置为"Unknown",创建如下看起来空白的文档:
{
"_id" : ObjectId("1"),
"Country" : "Unknown",
"Last Name" : "Wallace"
}
用$push 将值追加到指定的字段
PHP 的$push操作符允许您将一个值添加到指定的字段中。如果字段是现有数组,将添加数据;如果该字段不存在,将会创建它。如果该字段存在,但它不是一个数组,那么将引发一个错误条件。下面的例子展示了如何使用$push将一些数据添加到现有的数组中:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wood");
// Specify the information to be added
$update = array('$push' => array("E-Mail" => "vw40%mongo.db"));
// Perform the update
$collection->update($criteria,$update);
用\(push 和\)each 向一个键添加多个值
$push操作符还允许您向一个键追加多个值。为此,需要添加$each修饰符。如果给定字段中不存在数组中的值,则将添加这些值。由于使用了$push操作符,同样的一般规则也适用:如果字段存在,并且它是一个数组,那么数据将被添加;如果它不存在,那么它将被创建;如果它存在,但它不是一个数组,那么将引发一个错误条件。以下示例说明了如何使用$each修改器:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wood");
// Specify the information to be added
$update = array(
'$push' => array(
"E-Mail" => array(
'$each' => array(
"vicwo40%mongo.db",
"``vicwo40%example.com
)
)
)
);
// Perform the update
$collection->update($criteria,$update);
用$addToSet 向数组中添加数据
$addToSet操作符类似于$push操作符,有一个重要的区别:$addToSet确保只有当数据不在数组中时,才将数据添加到数组中。$addToSet运算符将一个数组作为参数:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wood");
// Specify the information to be added (successful because it doesn't exist yet)
$update = array('$addToSet' => array("E-Mail" => "``vic40%example.com
// Perform the update
$collection->update($criteria,$update);
类似地,您可以通过组合$addToSet操作符和$each操作符来添加一些尚不存在的项目:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wood");
// Specify the information to be added (partially successful; some
// examples were already there)
$update = array(
'$addToSet' => array
(
"E-Mail" => array
(
'$each' => array
(
"vw40%mongo.db",
"vicky40%mongo.db",
"``vicky40%example.com
)
)
)
);
// Perform the update
$collection->update($criteria,$update);
用$pop 从数组中移除元素
PHP 的$pop操作符允许你从数组中移除一个元素。请记住,您只能删除数组中的第一个或最后一个元素,而不能删除中间的任何元素。您通过指定值-1来删除第一个元素;类似地,通过指定1的值来删除最后一个元素:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wood");
// Pop out the first e-mail address found in the list
$update = array('$pop' => array("E-Mail" => -1));
// Perform the update
$collection->update($criteria,$update);
Note
指定值-2或1000不会改变删除哪个元素。任何负数都将删除第一个元素,而任何正数都将删除最后一个元素。使用值0从数组中删除最后一个元素。
使用$pull 删除每个值
您可以使用 PHP 的$pull操作符从数组中删除给定值的每一次出现。例如,如果您在使用$push或$pushAll时不小心将副本添加到数组中,这就很方便了。以下示例删除电子邮件地址的任何重复项:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wood");
// Pull out each occurrence of the e-mail address "``vicky40%example.com
$update = array('$pull' => array("E-Mail" => "``vicky40%example.com
// Perform the update
$collection->update($criteria,$update);
移除多个元素的每个匹配项
类似地,您可以使用$pullAll操作符从文档中删除多个元素的每次出现,如下例所示:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wood");
// Pull out each occurrence of the e-mail addresses below
$update = array(
'$pullAll' => array(
"E-Mail" => array("vw40%mongo.db","``vw40%office.com
)
);
// Perform the update
$collection->update($criteria,$update);
使用 save()更新数据
像insert()函数一样,save()函数允许您将数据插入到集合中。唯一的区别是,您还可以使用save()来更新已经保存数据的字段。您可能还记得,这被称为 upsert。在这一点上,您执行save()函数的方式不应该令人惊讶。像 MongoDB shell 中的save()函数一样,PHP 的save()接受两个参数:一个包含您希望保存的信息的数组,以及任何保存选项。可以使用以下选项:
fsync:如果设置为true,该布尔选项会在返回成功之前将数据同步到磁盘。如果该选项被设置为true,则意味着w被设置为0,即使它没有被设置。w:如果设置为 0,保存操作将不被确认。使用副本集时,w 也可以设置为 n,以确保主服务器在成功复制到 n 个节点时确认保存操作。也可以设置为'majority'—一个保留字符串,以确保大多数副本节点将确认保存,或者设置为特定标记,以确保标记的那些节点将确认保存。该选项默认为 1,表示确认保存操作。j:如果设置为 true,此布尔选项将在指示保存成功之前强制将数据写入日志。默认为 false。wtimeout:用于指定服务器等待接收确认的时间(毫秒)。默认为 10000。timeout:用于指定客户端需要等待数据库响应的时间(毫秒)。
PHP 的save()版本的语法类似于 MongoDB shell 中的语法,如下例所示:
// Specify the document to be saved
$contact = array(
"Given Name" => "Kenji",
"Family Name" => "Kitahara",
"Address" => array(
"Street" => "149 Bartlett Avenue",
"Place" => "Southfield",
"Postal Code" => "MI 48075",
"Country" => "USA"
)
,
"E-Mail" => array(
" kk40%example.com ",
"``kk40%office.com
),
"Phone" => "248-510-1562",
"Age" => 34
);
// Connect to the database
$c = new MongoClient();
// Select the collection 'people'
$collection = $c->contacts->people;
// Save via the save() function
$options = array("fsync" => true);
// Specify the save() options
$collection->save($contact,$options);
// Realizing you forgot something, let's upsert this contact:
$contact['Category'] = 'Work';
// Perform the upsert
$collection->save($contact);
原子地修改文档
像save()和update()函数一样,findAndModify()函数可以从 PHP 驱动程序中调用。请记住,您可以使用findAndModify()函数自动修改文档,并在更新成功执行后返回结果。您使用findAndModify()函数来更新单个文档——仅此而已。您可能还记得,默认情况下,返回的文档不会显示所做的修改——返回包含所做修改的文档需要指定一个额外的参数:参数new。
findAndModify函数有四个参数;query、update、fields、options。其中一些是可选的,取决于您的操作。例如,当指定update标准时,字段和选项是可选的。然而,当您希望使用remove选项时,需要指定update和fields参数(例如,使用null)。以下列表详细列出了可用的参数:
query:指定查询的过滤器。如果没有指定这个参数,那么集合中的所有文档都将被视为可能的候选文档,遇到的第一个文档将被更新或删除。update:指定更新文档的信息。请注意,前面指定的任何修饰运算符都可以用来实现这一点。fields:指定您希望看到返回的字段,而不是整个文档。该参数的行为与find()功能中的fields参数相同。请注意,_id字段将总是被返回,即使该字段不在您要返回的字段列表中。options:指定要应用的选项。可以使用以下选项:sort:按照指定的顺序对匹配的文档进行排序。remove:如果设置为true,将删除第一个匹配的文档。update:如果设置为true,将对选择的文档进行更新。new:如果设置为true,则返回更新的文档,而不是选择的文档。注意,这个参数不是默认设置的,这在某些情况下可能会有点混乱。upsert:如果设置为true,则执行一次上插。
现在让我们来看一组说明如何使用这些参数的例子。第一个示例搜索姓氏为"Kitahara"的联系人,并通过将update()和$push操作符结合起来,将一个电子邮件地址添加到他的联系人卡片中。在下面的例子中没有设置new参数,所以结果输出仍然显示旧的信息:
// Connect to the database
$c = new MongoClient();
// Specify the database and collection in which to work
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Kitahara");
// Specify the update criteria
$update = array('$push' => array("E-Mail" => "kitahara40%mongo.db"));
// Perform a findAndModify()
$collection->findAndModify($criteria,$update);
返回的结果如下所示:
Array (
[value] => Array (
[Given Name] => Kenji
[Family Name] => Kitahara
[Address] => Array (
[Street] => 149 Bartlett Avenue
[Place] => Southfield
[Postal Code] => MI 48075
[Country] => USA
)
[E-Mail] => Array (
[0] => kk40%example.com
[1] => kk40%office.com
)
[Phone] => 248-510-1562
[Age] => 34
[_id] => MongoId Object ( )
[Category] => Work
)
[ok] => 1
)
以下示例显示了如何使用remove和sort参数:
// Connect to the database
$c = new MongoClient();
// Specify the database and collection in which to work
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Category" => "Work");
// Specify the options
$options = array("sort" => array("Age" => -1), "remove" => true);
// Perform a findAndModify()
$collection->findAndModify($criteria,null,null,$options);
删除数据
您可以使用remove()函数从 MongoDB shell 中删除一个文档,就像前面例子中的文档一样。PHP 驱动程序还包括一个remove()函数,可以用来删除数据。这个函数的 PHP 版本有两个参数:一个包含要删除的记录的描述,另一个指定管理删除过程的附加选项。
有七个选项可供选择:
justOne:如果设置为 true,则最多只能删除一条符合条件的记录。fsync:如果设置为true,该布尔选项会在返回成功之前将数据同步到磁盘。如果该选项被设置为true,则意味着w被设置为0,即使它没有被设置。w:如果设置为 0,保存操作将不被确认。使用副本集时,w 也可以设置为 n,以确保主服务器在成功复制到 n 个节点时确认保存操作。也可以设置为'majority'—一个保留字符串,以确保大多数副本节点将确认保存,或者设置为特定的标记,以确保那些标记的节点将确认保存。该选项默认为 1,表示确认保存操作。j:如果设置为 true,此布尔选项将在指示保存成功之前强制将数据写入日志。默认为 false。wtimeout:用于指定服务器等待接收确认的时间(毫秒)。默认为 10000。timeout:用于指定客户端需要等待数据库响应的时间(毫秒)。
现在让我们来看几个说明如何移除文档的代码示例:
// Connect to the database
$c = new MongoClient();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria
$criteria = array("Family Name" => "Wallace");
// Specify the options
$options = array('justOne' => true, 'w' => 0);
// Perform the removal
$collection->remove($criteria,$options);
同样,下一个示例同时删除多个文档:
// Connect to the database
$c = new Mongo();
// Select the collection 'people' from the database 'contacts'
$collection = $c->contacts->people;
// Specify the search criteria using regular expressions
$criteria = array("E-Mail" => new MongoRegex("``/40%office.com/i
// Specify the options
$options = array("justOne" => false);
// Perform the removal
$collection->remove($criteria,$options);
Warning
当您删除文档时,请记住对该文档的任何引用都将保留在数据库中。请确保您也手动删除或更新对已删除文档的引用;否则,这些引用在求值时将返回null。
类似地,您可以使用drop()函数删除整个集合。以下示例返回包含删除结果的数组:
// Connect to the database
$c = new Mongo();
// Select the collection to remove
$collection = $c->contacts->people;
// Remove the collection and return the results
print_r($collection->drop());
返回的结果如下所示:
Array (
[nIndexesWas] => 1
[msg] => indexes dropped for collection
[ns] => contacts.people
[ok] => 1
)
最后但同样重要的是,您可以使用 PHP 删除整个数据库。您可以通过使用MongoDB类中的drop()函数来实现这一点,如下例所示:
// Connect to the database
$c = new Mongo();
// Select the database to remove
$db = $c->contacts;
// Remove the database and return the results
print_r($db->drop());
返回的结果显示被删除的数据库的名称:
Array (
[dropped] => contacts
[ok] => 1
)
其他序列库的有关记录
DBRef 使您能够在存储在不同位置的两个文档之间创建链接;该功能允许您实现类似于关系数据库中的行为。如果您想将来自这些人的地址存储在一个addresses集合中,而不是将这些信息包含在您的people集合中,这个功能会非常方便。
有两种方法可以做到这一点。首先,您可以使用一个简单的链接(称为手动引用);在这种情况下,您将一个文档的_id包含到另一个文档中。其次,可以使用 DBRef 自动创建这样的链接。
首先,让我们看看如何实现手动引用。在以下示例中,您添加了一个联系人,并在其地址信息下指定了另一个文档的_id:
// Connect to the database
$c = new MongoClient();
$db = $c->contacts;
// Select the collections we want to store our contacts and addresses in
$people = $db->people;
$addresses = $db->addresses;
// Specify an address
$address = array(
"Street" => "St. Annastraat 44",
"Place" => "Monster",
"Postal Code" => "2681 SR",
"Country" => "Netherlands"
);
// Save the address
$addresses->insert($address);
// Add a contact living at the address
$contact = array(
"First Name" => "Melvyn",
"Last Name" => "Babel",
"Age" => 35,
"Address" => $address['_id']
);
$people->insert($contact);
现在假设您想要查找前面的联系人的地址信息。为此,只需在address字段中查询Object ID;你可以在addresses收藏中找到这个信息(假设你知道这个收藏的名字)。
这是可行的,但是引用另一个文档的首选方法依赖于 DBRef。这是因为 DBRef 依赖于数据库和所有驱动程序都理解的通用格式。我们马上会看到前面例子的 DBRef 版本。不过,在这样做之前,我们先来看看DBRef类的create()函数;您将使用该类来创建所需的引用。
create()函数有三个参数:
collection:指定信息所在集合的名称(不含数据库名称)。id:指定要链接的文档的 ID。database:指定文档所在数据库的名称。
以下示例使用create()函数创建对另一个文档中地址的引用:
// Connect to the database
$c = new Mongo();
$db = $c->contacts;
// Select the collections we want to store our contacts and addresses in
$people = $db->people;
$addresses = $db->addresses;
// Specify an address
$address = array(
"Street" => "WA Visser het Hooftlaan 2621",
"Place" => "Driebergen",
"Postal Code" => "3972 SR",
"Country" => "Netherlands"
);
// Save the address
$addresses->insert($address);
// Create a reference to the address
$addressRef = MongoDBRef::create($addresses->getName(), $address['_id']);
// Add a contact living at the address
$contact = array(
"First Name" => "Ivo",
"Last Name" => "Lauw",
"Age" => 24,
"Address" => $addressRef
);
$people->insert($contact);
Note
本例中的getName()函数用于获取集合的名称。
检索信息
到目前为止,您已经使用 DBRef 创建了一个引用。现在是时候看看如何检索引用的信息了,这样您就可以再次正确地显示内容。使用 MongoDBRef 的get()函数可以做到这一点。
MongoDBRef 的get()函数有两个参数。第一个参数指定要使用的数据库,而第二个参数提供要提取的引用:
// Connect to the database
$c = new Mongo();
// Select the collection 'people' from the database 'contacts'
$people = $c->contacts->people;
// Define the search parameters
$lastname = array("Last Name" => "Lauw");
// Find our contact, and store under the $person variable
$person = $people->findOne(array("Last Name" => "Lauw"));
// Dereference the address
$address = MongoDBRef::get($people->db, $person['Address']);
// Print out the address from the matching contact
print_r($address);
结果输出显示了被引用的文档:
Array (
[_id] => MongoId Object ( )
[Street] => WA Visser het Hooftlaan 2621
[Place] => Driebergen
[Postal Code] => 3972 SR
[Country] => Netherlands
)
DBRef 提供了一种很好的方式来存储您想要引用的数据,尤其是因为它允许集合和数据库名称的灵活性。
GridFS 和 PHP 驱动程序
前一章详细介绍了 GridFS 及其优点。例如,除了其他与 GridFS 相关的技术之外,它还解释了如何使用这种技术来存储和检索数据。在本节中,您将学习如何使用 PHP 驱动程序通过 GridFS 存储和检索文件。
PHP 驱动程序包含自己的处理 GridFS 的类;下面是三个最重要的类及其作用:
MongoGridFS:从数据库中存储和检索文件。这个类包含几个方法,包括delete()、find()、storeUpload()和大约六个其他方法。MongoGridFSFile:作用于数据库中的特定文件。包括__construct()、getFilename()、getSize()、write()等功能。MongoGridFSCursor:作用于光标。它包含了一些功能,如__construct()、current()、getNext()和key()。
让我们看看如何使用 PHP 将文件上传到数据库中。
Note
如果没有上载数据的 HTML 表单,下面示例中的代码将无法运行。然而,这样的代码超出了本章的范围,所以这里没有给出。
存储文件
使用 GridFS 使用storeUpload()函数将文件存储到数据库中。这个函数有两个参数:一个表示要上传的文件的字段名,另一个可选地用于指定文件的元数据。一旦使用,该函数将报告存储文件的_id。
以下简单的代码示例显示了如何使用storeUpload()函数:
// Connect to the database
$c = new MongoClient();
// Select the name of the database
$db = $c->contacts;
// Define the GridFS class to ensure we can handle the files
$gridFS = $db->getGridFS();
// Specify the HTML field's name attribute
$file = 'fileField';
// Specify the file's metadata (optional)
$metadata = array('uploadDate' => date());
// Upload the file into the database
$id = $gridFS->storeUpload($file,$metadata);
这就是全部了。正如你所看到的,$id被用作参数来存储数据库中的文件。您也可以使用此参数通过 DBRef 引用数据。
向存储的文件添加更多元数据
有时,您可能想要向存储的文件中添加更多元数据。默认情况下,添加的唯一其他数据是_id字段,当您将图片存储到联系人卡片时,可能会使用该字段来引用数据。不幸的是,当您想开始通过这些标签搜索数据时,这可能会被证明是一种限制而不是一种好处。
以下示例显示了如何存储上传数据的元数据。这个例子建立在前面的代码块之上,特别是参数$id。显然,您可以使用任何其他所需的搜索标准自行定制:
// Specify the metadata to be added
$metadata = array('$set' => array("Tag" => "Avatar"));
// Specify the search criteria to which to apply the metadata
$criteria = array('_id' => $id);
// Insert the metadata
$db->grid->update($criteria, $metadata);
正在检索文件
当然,如果您以后不能检索这些文件,那么将文件存储在数据库中的能力对您没有任何好处。检索文件几乎一样困难(阅读:容易!)存储它们。让我们看两个例子:第一个检索存储的文件名,而第二个检索文件本身。
以下示例显示了如何检索存储的文件名,这是使用getFilename()函数完成的:
// Connect to the database
$c = new MongoClient();
$db = $c->contacts;
// Initialize GridFS
$gridFS = $db->getGridFS();
// Find all files in the GridFS storage and store under the $cursor parameter
$cursor = $gridFS->find();
// Return all the names of the files
foreach ($cursor as $object) {
echo "Filename:".$object->getFilename();
}
那很容易!当然,这个例子假设您的数据库中存储了一些数据。在添加了一点数据之后,或者如果您想要搜索更具体的数据,您可能还想向find()函数添加更多的搜索参数。请注意,find()函数搜索添加到每个上传文件的元数据(如本章前面所述)。
您可能想知道如何检索文件本身。毕竟,检索数据可能是您最终使用最多的东西。您可以通过使用getBytes()函数将它发送到您的浏览器来实现这一点。下一个例子使用getBytes()函数来检索先前存储的图像。注意,您可以通过查询数据库来检索_id参数(下面的示例只是虚构了一些参数)。此外,必须指定内容类型,因为从逻辑上讲,当您再次构建数据时,浏览器不会识别内容类型:
// Connect to the database
$c = new MongoClient();
// Specify the database name
$db = $c->contacts;
// Initialize the GridFS files collection
$gridFS = $db->getGridFS();
// Specify the search parameter for the file
$id = new MongoId('4c555c70be90968001080000');
// Retrieve the file
$file = $gridFS->findOne(array('_id' => $id));
// Specify the header for the file and write the data to it
header('Content-Type: image/jpeg');
echo $file->getBytes();
exit;
删除数据
您可以使用delete()功能确保删除任何先前存储的数据。这个函数有一个参数:文件本身的_id。下面的例子说明了如何使用delete()函数删除匹配对象 ID 4c555c70be90968001080000 的文件:
// Connect to the database
$c = new MongoClient();
// Specify the database name
$db = $c->contacts;
// Initialize GridFS
$gridFS = $db->getGridFS();
// Specify the file via it's ID
$id = new MongoId('4c555c70be90968001080000');
$file = $gridFS->findOne(array('_id' => $id));
// Remove the file using the remove() function
$gridFS->delete($id);
摘要
在本章中,您已经深入了解了如何使用 MongoDB 的 PHP 驱动程序。例如,您已经看到了如何在 PHP 驱动程序中使用最常用的函数,包括insert()、update()和modify()函数。您还学习了如何通过使用 PHP 驱动程序的find()函数来搜索文档。最后,您学习了如何利用 DBRef 的功能,以及如何使用 GridFS 存储和检索文件。
一章不可能涵盖关于使用 MongoDB PHP 驱动程序的所有知识;尽管如此,这一章应该提供必要的基础知识来执行你想用这个驱动程序完成的大多数操作。在这一过程中,您还学会了在事情变得稍微复杂一点的时候使用服务器端命令。
在下一章中,你将探索相同的概念,但是对于 Python 驱动程序。
七、Python 和 MongoDB
Abstract
Python 是比较容易学习和掌握的编程语言之一。如果你相对来说是编程新手,这是一门非常好的语言。如果你已经非常熟悉编程,你会更快地掌握它。
Python 是比较容易学习和掌握的编程语言之一。如果你相对来说是编程新手,这是一门非常好的语言。如果你已经非常熟悉编程,你会更快地掌握它。
Python 可以用来快速开发应用,同时确保代码本身保持良好的可读性。记住这一点,本章将向您展示如何通过 Python 驱动程序(也称为 PyMongo 驱动程序;本章将交替使用这两个术语)。
首先,您将看到Connection()函数,它使您能够建立到数据库的连接。其次,你将学习如何写文档或字典,以及如何插入它们。第三,您将学习如何使用 Python 驱动程序使用find()或find_one()命令来检索文档。这两个命令都可以选择使用一组丰富的查询修饰符来缩小搜索范围,使查询更容易实现。第四,您将了解用于执行更新的各种操作符。最后,您将了解如何使用 PyMongo 在文档甚至数据库级别删除数据。作为额外的收获,您将学习如何使用DBRef模块来引用存储在其他地方的数据。
我们开始吧。
Note
在这一章中,你会看到许多实际的代码例子来说明所讨论的概念。代码本身前面会有三个大于号(>>>),表示该命令是用 Python shell 编写的。查询代码将以粗体显示,结果输出将以明文呈现。
使用 Python 处理文档
如前几章所述,MongoDB 使用 BSON 风格的文档,PHP 使用关联数组。同样,Python 也有它所谓的字典。如果你已经玩过 MongoDB 控制台,我们相信你一定会爱上 Python。毕竟,语法是如此的相似,以至于语言语法的学习曲线可以忽略不计。
我们已经在前一章中介绍了 MongoDB 文档的结构,所以这里不再重复。相反,让我们来看看文档在 Python shell 中是什么样子的:
item = {
"Type" : "Laptop",
"ItemNumber" : "1234EXD",
"Status" : "In use",
"Location" : {
"Department" : "Development",
"Building" : "2B",
"Floor" : 12,
"Desk" : 120101,
"Owner" : "Anderson, Thomas"
},
"Tags" : ["Laptop","Development","In Use"]
}
虽然您应该记住 Python 术语词典,但在大多数情况下,本章将引用它的 MongoDB 等价物 document。毕竟,大多数时候,我们将使用 MongoDB 文档。
使用 PyMongo 模块
Python 驱动程序使用模块。您可以像对待 PHP 驱动程序中的类一样对待这些类。PyMongo 驱动程序中的每个模块负责一组操作。以下每个任务都有一个单独的模块(还有很多):建立连接、使用数据库、利用集合、操作光标、使用DBRef模块、转换ObjectId以及运行服务器端 JavaScript 代码。
本章将带您了解使用 PyMongo 驱动程序所需的最基本也是最有用的操作。一步一步地,您将学习如何通过简单易懂的代码片段使用命令,您可以将这些代码片段直接复制并粘贴到 Python shell(或脚本)中。从这里开始,这是管理 MongoDB 数据库的第一步。
连接和断开
建立到数据库的连接需要首先导入 PyMongo 驱动程序的MongoClient模块,这使您能够建立连接。在 shell 中键入以下语句来加载MongoClient模块:
>>> from pymongo import MongoClient
一旦您的 MongoDB 服务启动并运行(如果您希望连接,这是强制性的),您就可以通过调用MongoClient()函数建立到服务的连接。
如果没有给出额外的参数,该函数假设您想要连接到本地主机上的服务(本地主机的默认端口号是 27017)。下面一行建立了连接:
>>> c = MongoClient()
您可以看到连接通过 MongoDB 服务 shell 进入。一旦建立了连接,就可以使用c字典来引用该连接,就像在 shell 中使用db和在 PHP 中使用$c一样。接下来,选择您想要使用的数据库,将该数据库存储在db字典下。您可以像在 MongoDB shell 中那样做——在下面的例子中,您使用了inventory数据库:
>>> db = c.inventory
>>> db
Database(Connection('localhost', 27017), u'inventory')
本例中的输出显示您已经连接到本地主机,并且正在使用inventory数据库。
现在已经选择了数据库,您可以用完全相同的方式选择您的 MongoDB 集合。因为您已经在db字典下存储了数据库名称,所以您可以使用它来选择集合的名称;在这种情况下称为items:
>>> collection = db.items
插入数据
剩下的工作就是通过将文档存储在字典中来定义它。让我们以前面的例子为例,将它插入到 shell 中:
>>> item = {
... "Type" : "Laptop",
... "ItemNumber" : "1234EXD",
... "Status" : "In use",
... "Location" : {
... "Department" : "Development",
... "Building" : "2B",
... "Floor" : 12,
... "Desk" : 120101,
... "Owner" : "Anderson, Thomas"
... },
... "Tags" : ["Laptop","Development","In Use"]
... }
一旦定义了文档,就可以使用 MongoDB shell 中提供的相同的insert()函数来插入它:
>>> collection.insert(item)
ObjectId('4c57207b4abffe0e0c000000')
这就是全部内容:您定义文档并使用insert()函数插入它。
在插入文档时,还有一个更有趣的技巧可以利用:同时插入多个文档。您可以通过在单个字典中指定两个文档,然后插入该文档来实现这一点。结果将返回两个ObjectId值;请仔细注意在以下示例中如何使用括号:
>>> two = [{
... "Type" : "Laptop",
... "ItemNumber" : "2345FDX",
... "Status" : "In use",
... "Location" : {
... "Department" : "Development",
... "Building" : "2B",
... "Floor" : 12,
... "Desk" : 120102,
... "Owner" : "Smith, Simon"
... },
... "Tags" : ["Laptop","Development","In Use"]
... },
... {
... "Type" : "Laptop",
... "ItemNumber" : "3456TFS",
... "Status" : "In use",
... "Location" : {
... "Department" : "Development",
... "Building" : "2B",
... "Floor" : 12,
... "Desk" : 120103,
... "Owner" : "Walker, Jan"
... },
... "Tags" : ["Laptop","Development","In Use"]
... }]
>>> collection.insert(two)
[ObjectId('4c57234c4abffe0e0c000001'), ObjectId('4c57234c4abffe0e0c000002')]
查找您的数据
PyMongo 提供了两个函数来查找您的数据:find_one(),它在您的集合中查找符合指定标准的单个文档;和find(),它可以根据提供的参数找到多个文档(如果不指定任何参数,find()匹配集合中的所有文档)。让我们看一些例子。
查找单个文档
正如刚才提到的,您使用find_one()函数来查找单个文档。该函数类似于 MongoDB shell 中的findOne()函数,因此掌握它的工作原理应该不会对您构成太大的挑战。默认情况下,如果在没有任何参数的情况下执行,该函数将返回集合中的第一个文档,如下例所示:
>>> collection.find_one()
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'1234EXD',
u'Location':{
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Anderson, Thomas',
u'Desk': 120101
},
u'_id': ObjectId('4c57207b4abffe0e0c000000'),
u'Type': u'Laptop'
}
您可以指定附加参数,以确保返回的第一个文档与您的查询相匹配。用于find()功能的每个参数也可用于find_one(),尽管limit参数将被忽略。需要编写查询参数,就像在 shell 中定义它们一样;也就是说,您需要指定一个键及其值(或多个值)。例如,假设您想要查找一个文档,其ItemNumber的值为3456TFS,并且您不想返回该文档的 _ id。以下查询实现了这一点,并返回如下所示的输出:
>>> collection.find_one({"ItemNumber" : "3456TFS"} ,fields={'_id' : False})
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'3456TFS',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Walker, Jan',
u'Desk': 120103
},
u'Type': u'Laptop'
}
Note
Python 是区分大小写的。因此,true和false并不等同于True和False。
如果搜索条件对于一个文档来说比较常见,您还可以指定附加的查询运算符。例如,假设查询{"Department" : "Development"},它将返回多个结果。我们马上就会看到这样一个例子。然而,首先让我们确定如何返回多个文档,而不仅仅是一个。这可能和你想象的有点不一样。
查找多个文档
您需要使用find()函数来返回多个文档。到目前为止,您可能已经在 MongoDB 中使用过这个命令很多次了,所以您可能对它已经相当熟悉了。Python 中的概念是一样的:在括号中指定查询参数来查找指定的信息。
然而,将结果返回到屏幕上的工作方式有所不同。就像在 PHP 和 shell 中工作一样,查询一组文档会返回一个游标实例。然而,与在 shell 中输入不同,您不能简单地输入db.items.find()来显示所有结果。相反,您需要使用光标检索所有文档。以下示例显示了如何显示来自items集合的所有文档(注意,您之前定义了collection来匹配集合的名称;为了清楚起见,省略了结果):
>>> for doc in collection.find():
... doc
...
请密切注意单词doc前的缩进。如果不使用这个缩进,那么将显示一个错误消息,说明没有出现预期的缩进块。Python 的优势之一是使用这种缩进方法作为块分隔符,因为这种方法可以保持代码有序。请放心,您会相对较快地习惯这种 Pythonic 编码惯例。但是,如果您碰巧忘记了缩进,您将会看到一条类似如下的错误消息:
File "<stdin>", line 2
doc
^
IndentationError: expected an indented block
接下来,让我们看看如何使用find()函数指定查询操作符。为此使用的方法与本书前面提到的方法相同:
>>> for doc in collection.find({"Location.Owner" : "Walker, Jan"}):
... doc
...
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'3456TFS',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Walker, Jan',
u'Desk': 120103
},
u'_id': ObjectId('4c57234c4abffe0e0c000002'),
u'Type': u'Laptop'
}
使用点符号
点符号用于搜索嵌入对象中的匹配元素。前面的代码片段显示了如何做到这一点的示例。使用这种技术时,您只需在嵌入对象中指定一个项的键名来搜索它,如下例所示:
>>> for doc in collection.find({"Location.Department" : "Development"}):
... doc
...
本示例返回任何设置了Development部门的文档。当在一个简单的数组中搜索信息时(例如,应用的标签),您只需要填写任何匹配的标签:
>>> for doc in collection.find({"Tags" : "Laptop"}):
... doc
...
返回字段
如果您的文档相对较大,并且您不希望返回存储在文档中的所有键/值信息,那么您可以在find()函数中包含一个额外的参数,以指定只有特定的一组字段需要使用True返回,或者使用False保持隐藏。您可以通过提供fields参数来实现这一点,然后在搜索条件后提供一个字段名列表。请注意,在_id字段之外,您不能在一个查询中混合使用True和False。
以下示例仅返回当前所有者的姓名、物品编号和对象 ID(这将始终被返回,除非您明确告诉 MongoDB 不要返回):
>>> for doc in collection.find({'Status' : 'In use'}, fields={'ItemNumber' : True, 'Location.Owner' : True}):
... doc
...
{
u'ItemNumber': u'1234EXD',
u'_id': ObjectId('4c57207b4abffe0e0c000000'),
u'Location': {
u'Owner': u'Anderson, Thomas'
}
}
{
u'ItemNumber': u'2345FDX',
u'_id': ObjectId('4c57234c4abffe0e0c000001'),
u'Location': {
u'Owner': u'Smith, Simon'
}
}
{
u'ItemNumber': u'3456TFS',
u'_id': ObjectId('4c57234c4abffe0e0c000002'),
u'Location': {
u'Owner': u'Walker, Jan'
}
}
我想你会同意这种指定标准的方法非常方便。
用 sort()、limit()和 skip()简化查询
sort()、limit()和skip()函数使得实现查询更加容易。单独来看,这些功能都有其用途,但是将它们结合起来会使它们变得更好、更强大。您可以使用sort()功能按特定键对结果进行排序;用于限制返回结果总数的limit()函数;和skip()函数,在返回匹配查询的剩余文档之前跳过找到的前 n 个项目。
让我们看一组单独的例子,从sort()函数开始。为了节省空间,下面的示例包含另一个参数,以确保只返回几个字段:
>>> for doc in collection.find ({'Status' : 'In use'},
... fields={'ItemNumber' : True, 'Location.Owner' : True}).sort(‘ItemNumber’):
... doc
...
{
u'ItemNumber': u'1234EXD',
u'_id': ObjectId('4c57207b4abffe0e0c000000'),
u'Location': {
u'Owner': u'Anderson, Thomas'
}
}
{
u'ItemNumber': u'2345FDX',
u'_id': ObjectId('4c57234c4abffe0e0c000001'),
u'Location': {
u'Owner': u'Smith, Simon'
}
}
{
u'ItemNumber': u'3456TFS',
u'_id': ObjectId('4c57234c4abffe0e0c000002'),
u'Location': {
u'Owner': u'Walker, Jan'
}
}
接下来,我们来看看limit()函数的运行情况。在这种情况下,您告诉函数只返回它在集合中找到的前两个条目中的ItemNumber(注意,在这个例子中没有指定搜索条件):
>>> for doc in collection.find({}, {"ItemNumber" : "true"}).limit(2):
... doc
...
{u'ItemNumber': u'1234EXD', u'_id': ObjectId('4c57207b4abffe0e0c000000')}
{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c57234c4abffe0e0c000001')}
在返回一组文档之前,您可以使用skip()功能跳过一些项目,如下例所示:
>>> for doc in collection.find({}, {"ItemNumber" : "true"}).skip(2):
... doc
...
{u'ItemNumber': u'3456TFS', u'_id': ObjectId('4c57234c4abffe0e0c000002')}
您也可以结合使用这三种功能,只选择找到的一定数量的项目,同时指定要跳过的特定数量的项目,并对它们进行排序:
>>> for doc in collection.find( {'Status' : 'In use'},
... fields={'ItemNumber' : True, 'Location.Owner' : True}).limit(2).skip(1).sort 'ItemNumber'):
... doc
...
{
u'ItemNumber': u'2345FDX',
u'_id': ObjectId('4c57234c4abffe0e0c000001'),
u'Location': {
u'Owner': u'Smith, Simon'
}
}
{
u'ItemNumber': u'3456TFS',
u'_id': ObjectId('4c57234c4abffe0e0c000002'),
u'Location': {
u'Owner': u'Walker, Jan'
}
}
您刚才所做的——限制返回的结果并跳过一定数量的项目——通常被称为分页。你可以用一种稍微简单一点的方式用$slice操作符来完成,这将在本章的后面讨论。
聚合查询
如前所述,MongoDB 附带了一套强大的聚合工具(关于这些工具的更多信息,请参见第 4 章)。您可以在 Python 驱动程序中使用所有这些工具。这些工具使得使用count()函数对数据进行计数成为可能;使用distinct()函数获得没有重复的不同值的列表;最后但同样重要的是,使用map_reduce()功能对数据进行分组,并对结果进行批量处理,或者只是进行计数。
这组命令,单独或一起使用,使您能够有效地查询您需要知道的信息,而不是别的。
除了这些基本的聚合命令,PyMongo 驱动程序还包括聚合框架。这个强大的功能将允许您计算聚合值,而无需使用通常过于复杂的 map/reduce(或 MapReduce)框架。
使用 count()计数项目
如果您只想计算符合条件的项目总数,您可以使用count()功能。该函数不像find()函数那样返回所有信息;相反,它返回一个整数值,其中包含找到的项目总数。
让我们看一些简单的例子。我们可以从返回整个集合中的文档总数开始,不指定任何标准:
>>> collection.count()
3
您还可以更精确地指定这些计数查询,如下例所示:
>>> collection.find({"Status" : "In use", "Location.Owner" : "Walker, Jan"}).count()
1
当您只需要快速计算符合您的标准的文档总数时,count()功能会非常有用。
使用 distinct()对唯一项目进行计数
count()函数是获得返回项目总数的一个很好的方法。但是,有时您可能会意外地将重复项添加到您的集合中,因为您只是忘记了删除或更改一个旧文档,并且您希望获得没有重复项的准确计数。这就是distinct()函数可以帮你的地方。该函数确保只返回唯一的项目。让我们通过向集合中添加另一个项目来设置一个示例,但是使用之前使用的ItemNumber:
>>> dup = ( {
"ItemNumber" : "2345FDX",
"Status" : "Not used",
"Type" : "Laptop",
"Location" : {
"Department" : "Storage",
"Building" : "1A"
},
"Tags" : ["Not used","Laptop","Storage"]
} )
>>> collection.insert(dup)
ObjectId('4c592eb84abffe0e0c000004')
当您在此时使用count()功能时,唯一项目的数量将不正确:
>>> collection.find({}).count()
4
相反,您可以使用distinct()函数来确保忽略任何重复项:
>>> collection.distinct("ItemNumber")
[u'1234EXD', u'2345FDX', u'3456TFS']
使用聚合框架对数据进行分组
聚合框架是一个计算聚合值的好工具,不需要使用 MapReduce。虽然 MapReduce 非常强大——并且可用于 PyMongo 驱动程序——但是聚合框架同样可以完成大多数工作,而且性能更好。为了演示这一点,aggregate()函数中最强大的管道操作符之一$group将用于按照标签对之前添加的文档进行分组,并使用$sum聚合表达式对其进行计数。让我们看一个例子:
>>> collection.aggregate([
... {'$unwind' : '$Tags'},
... {'$group' : {'_id' : '$Tags', 'Totals' : {'$sum' : 1}}}
... ])
首先,aggregate()函数使用$unwind管道操作符从文档的'$Tags数组(注意其名称中的强制$)创建一个标记文档流。接下来,调用$group管道操作符,使用它的值作为它的'_id'和总计数,为每个唯一标签创建一个单独的行——使用$group的$sum表达式计算'Totals'值。结果输出如下所示:
{
u'ok': 1.0,
u'result': [
{u'_id': u'Laptop', u'Totals': 4},
{u'_id': u'In Use', u'Totals': 3},
{u'_id': u'Development', u'Totals': 3},
{u'_id': u'Storage', u'Totals': 1},
{u'_id': u'Not used', u'Totals': 1}
]
}
输出准确返回所请求的信息。但是,如果我们希望按'Totals''对输出进行排序呢?这可以通过简单地添加另一个管道操作符$sort来实现。但是,在这样做之前,我们需要导入 SON 模块:
>>> from bson.son import SON
现在,我们可以根据'Totals'值按降序(-1)对结果进行排序,如下所示:
>>> collection.aggregate([
... {'$unwind' : '$Tags'},
... {'$group' : {'_id' : '$Tags', 'Totals' : {'$sum' : 1}}},
... {'$sort' : SON([('Totals', -1)])}
... ])
这将返回以下结果,以降序排列:
{
u'ok': 1.0,
u'result': [
{u'_id': u'Laptop', u'Totals': 4},
{u'_id': u'In Use', u'Totals': 3},
{u'_id': u'Development', u'Totals': 3},
{u'_id': u'Storage', u'Totals': 1},
{u'_id': u'Not used', u'Totals': 1}
]
}
除了$sum管道表达式之外,$group管道操作符还支持各种其他表达式,下面列出了其中一些:
$push:创建并返回一个数组,其中包含在它的组中找到的所有值。$addToSet:创建并返回一个数组,其中包含在该组中找到的所有唯一值。$first:仅返回其组中找到的第一个值。$last:仅返回其组中找到的最后一个值。$max:返回其组中找到的最大值。$min:返回其组中找到的最小值。$avg:返回其所在组的平均值。
在这个例子中,您已经看到了$group、$unwind和$sort管道操作符,但是还有许多更强大的管道操作符,比如$geoNear操作符。聚合框架及其操作符将在第 4、6 和 8 章中详细讨论。
使用提示()指定索引
您可以使用hint()函数来指定查询数据时应该使用哪个索引。使用此函数可以帮助您提高查询的性能。在 Python 中,hint()函数也在光标上执行。但是,您应该记住,您在 Python 中指定的提示名称需要与您传递给create_index()函数的名称相同。
在下一个示例中,您将首先创建一个索引,然后搜索指定该索引的数据。然而,在可以按升序排序之前,您需要使用import()函数来导入ASCENDING方法。最后,您需要执行create_index()功能:
>>> from pymongo import ASCENDING
>>> collection.create_index([("ItemNumber", ASCENDING)])
u'ItemNumber_1'
>>> for doc in collection.find({"Location.Owner" : "Walker, Jan"}) .hint([("ItemNumber", ASCENDING)]):
... doc
...
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'3456TFS',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Walker, Jan',
u'Desk': 120103
},
u'_id': ObjectId('4c57234c4abffe0e0c000002'),
u'Type': u'Laptop'
}
当您的集合不断增长时,使用索引可以帮助您显著提高性能速度(有关性能调优的更多详细信息,请参见第 10 章)。
用条件运算符细化查询
您可以使用条件运算符来优化您的查询。MongoDB 包括六个以上可通过 PyMongo 访问的条件操作符;这些和你在前面章节中看到的条件操作符是一样的。下面几节将带您浏览 Python 中可用的条件操作符,以及如何使用它们来优化 Python 中的 MongoDB 查询。
使用\(lt、\)gt、\(lte 和\)gte 运算符
让我们从查看$lt、$gt、$lte和$gte条件操作符开始。您可以使用$lt操作符来搜索任何小于 n 的数字信息。该操作符只接受一个参数:数字n,它指定了限制。下面的示例查找书桌号小于 120102 的所有条目。请注意,不包括比较值本身:
>>> for doc in collection.find({"Location.Desk" : {"$lt" : 120102} }):
... doc
...
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'1234EXD',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Anderson, Thomas',
u'Desk': 120101
},
u'_id': ObjectId('4c57207b4abffe0e0c000000'),
u'Type': u'Laptop'
}
同样,您可以使用$gt操作符来查找任何值高于所提供的比较值的项目。再次注意,不包括比较值本身:
>>> for doc in collection.find({"Location.Desk" : {"$gt" : 120102} }):
... doc
...
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'3456TFS',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Walker, Jan',
u'Desk': 120103
},
u'_id': ObjectId('4c57234c4abffe0e0c000002'),
u'Type': u'Laptop'
}
如果您想在结果中包含比较值,那么您可以使用$lte或$gte操作符分别查找任何小于或等于 n 或大于或等于 n 的值。以下示例说明了如何使用这些运算符:
>>> for doc in collection.find({"Location.Desk" : {"$lte" : 120102} }):
... doc
...
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'1234EXD',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Anderson, Thomas',
u'Desk': 120101
},
u'_id': ObjectId('4c57207b4abffe0e0c000000'),
u'Type': u'Laptop'
}
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'2345FDX',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Smith, Simon',
u'Desk': 120102
},
u'_id': ObjectId('4c57234c4abffe0e0c000001'),
u'Type': u'Laptop'
}
>>> for doc in collection.find({"Location.Desk" : {"$gte" : 120102} }):
... doc
...
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'2345FDX',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Smith, Simon',
u'Desk': 120102
},
u'_id': ObjectId('4c57234c4abffe0e0c000001'),
u'Type': u'Laptop'
}
{
u'Status': u'In use',
u'Tags': [u'Laptop', u'Development', u'In Use'],
u'ItemNumber': u'3456TFS',
u'Location': {
u'Department': u'Development',
u'Building': u'2B',
u'Floor': 12,
u'Owner': u'Walker, Jan',
u'Desk': 120103
},
u'_id': ObjectId('4c57234c4abffe0e0c000002'),
u'Type': u'Laptop'
}
搜索与$ne 不匹配的值
您可以使用$ne(不等于)运算符来搜索集合中不符合指定标准的任何文档。该操作符需要一个参数,即文档不应该具有的键和值信息,以使结果返回匹配:
>>> collection.find({"Status" : {"$ne" : "In use"}}).count()
1
用$in 指定匹配数组
$in操作符允许您指定一个可能匹配的数组。
例如,假设您只寻找两种开发计算机:not used或带有Development。还假设您希望将结果限制为两项,只返回ItemNumber:
>>> for doc in collection.find({"Tags" : {"$in" : ["Not used","Development"]}} , fields={"ItemNumber":"true"}).limit(2):
... doc
...
{u'ItemNumber': u'1234EXD', u'_id': ObjectId('4c57207b4abffe0e0c000000')}
{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c57234c4abffe0e0c000001')}
使用$nin 对匹配数组进行指定
使用$nin操作符和使用$in操作符完全一样;不同之处在于,该操作符排除了与给定数组中指定的任何值相匹配的任何文档。例如,以下查询查找Development部门当前未使用的任何项目:
>>> for doc in collection.find({"Tags" : {"$nin" : ["Development"]}}, fields={"ItemNumber": True}):
... doc
...
{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c592eb84abffe0e0c000004')}
查找与数组值匹配的文档
$in操作符可以用来查找与数组中指定的任何值相匹配的任何文档,而$all操作符可以让您查找与数组中指定的所有值相匹配的任何文档。完成此操作的语法看起来完全相同:
>>> for doc in collection.find({"Tags" : {"$all" : ["Storage","Not used"]}}, fields={"ItemNumber":"true"}):
... doc
...
{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c592eb84abffe0e0c000004')}
指定多个表达式与$或匹配
您可以使用$or操作符来指定一个文档可以拥有的多个值,其中至少有一个值必须为真,才符合匹配条件。这大致类似于$in操作符;不同之处在于$or操作符允许您指定键和值。您还可以将$or操作符与另一个键/值对结合使用。我们来看几个例子。
本示例将位置设置为Storage或所有者设置为Anderson, Thomas的所有文档返回到:
>>> for doc in collection.find({"$or" : [ { "Location.Department" : "Storage" },
... { "Location.Owner" : "Anderson, Thomas"} ] } ):
... doc
...
您也可以将上述代码与另一个键/值对结合使用,如下例所示:
>>> for doc in collection.find({ "Location.Building" : "2B", "$or" : [ { "Location.Department" : "Storage" },
... { "Location.Owner" : "Anderson, Thomas"} ] } ):
... doc
...
$or操作符基本上允许您同时进行两个搜索,并合并结果输出,即使单个搜索彼此没有任何共同点。另外,$or子句是并行执行的,每个子句可能使用不同的索引。
用$slice 从数组中检索项目
您可以使用$slice操作符从文档的给定数组中检索一定数量的项目。该运算符提供类似于skip()和limit()功能的功能;区别在于这两个函数作用于整个文档,而$slice操作符作用于单个文档中的数组。
在查看示例之前,让我们添加一个新文档,以便更好地了解这个操作符。假设你的公司疯狂地痴迷于跟踪它的椅子库存,跟踪它们可能去的任何地方。自然地,每把椅子都有它曾经所属的桌子的历史。$slice示例操作符非常适合跟踪这种库存。
首先添加以下文档:
>>> chair = ({
... "Status" : "Not used",
... "Tags" : ["Chair","Not used","Storage"],
... "ItemNumber" : "6789SID",
... "Location" : {
... "Department" : "Storage",
... "Building" : "2B"
... },
... "PreviousLocation" :
... [ "120100","120101","120102","120103","120104","120105",
... "120106","120107","120108","120109","120110" ]
... })
>>> collection.insert(chair)
ObjectId('4c5973554abffe0e0c000005')
现在假设您想要查看上一个示例中返回的椅子的所有可用信息,但有一点需要注意:您不想查看所有以前的位置信息,而只想查看它所属的前三张桌子:
>>> collection.find_one({'ItemNumber' : '6789SID'}, {'PreviousLocation' : {'$slice' : 3} })
{
u'Status': u'Not used',
u'PreviousLocation': [u'120100', u'120101', u'120102'],
u'Tags': [u'Chair', u'Not used', u'Storage'],
u'ItemNumber': u'6789SID',
u'Location': {
u'Department': u'Storage',
u'Building': u'2B'
},
u'_id': ObjectId('4c5973554abffe0e0c000005')
}
类似地,通过使整数值为负,可以看到它最近的三个位置:
>>> collection.find_one({'ItemNumber' : '6789SID'}, {'PreviousLocation' : {'$slice' : -3} })
{
u'Status': u'Not used',
u'PreviousLocation': [u'120108', u'120109', u'120110'],
u'Tags': [u'Chair', u'Not used', u'Storage'],
u'ItemNumber': u'6789SID',
u'Location': {
u'Department': u'Storage',
u'Building': u'2B'
},
u'_id': ObjectId('4c5973554abffe0e0c000005')
}
或者,您可以跳过椅子的前五个位置,将返回的结果数量限制为三个(特别注意这里的括号):
>>> collection.find_one({'ItemNumber' : '6789SID'}, {'PreviousLocation' : {'$slice' : [5, 3] } })
{
u'Status': u'Not used',
u'PreviousLocation': [u'120105', u'120106', u'120107'],
u'Tags': [u'Chair', u'Not used', u'Storage'],
u'ItemNumber': u'6789SID',
u'Location': {
u'Department': u'Storage',
u'Building': u'2B'
},
u'_id': ObjectId('4c5973554abffe0e0c000005')
}
你大概明白了。这个例子可能看起来有点不寻常,但库存控制系统经常转向非正统;并且$slice操作者天生擅长帮助你考虑不寻常或复杂的情况。例如,$slice操作符可能是实现网站评论区分页系统的一个特别有效的工具。
使用正则表达式进行搜索
进行搜索的一个有用工具是正则表达式。Python 的默认正则表达式模块叫做re。使用re模块执行搜索需要首先加载该模块,如下例所示:
>>> import re
加载模块后,您可以在搜索条件的value字段中指定正则表达式查询。下面的例子显示了如何搜索任何文档,其中ItemNumber的值包含一个4(为了简单起见,这个例子只返回ItemNumber)中的值:
>>> for doc in collection.find({'ItemNumber' : re.compile('4')}, {'ItemNumber' : True}):
... doc
...
{u'ItemNumber': u'1234EXD', u'_id': ObjectId('4c57207b4abffe0e0c000000')}
{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c57234c4abffe0e0c000001')}
{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c592eb84abffe0e0c000004')}
{u'ItemNumber': u'3456TFS', u'_id': ObjectId('4c57234c4abffe0e0c000002')}
您可以进一步定义正则表达式。在这个阶段,您的查询是区分大小写的,它将匹配任何在值ItemNumber中有一个4的文档,不管它的位置如何。但是,假设您想要查找一个文档,其中的ItemNumber值以FS结尾,前面是一个未知值,并且在FS之后不能包含任何附加数据:
>>> for doc in collection.find({'ItemNumber' : re.compile('.FS$')},fields={'ItemNumber' : True}):
... doc
...
{u'ItemNumber': u'3456TFS', u'_id': ObjectId('4c57234c4abffe0e0c000002')}
您也可以使用find()以不区分大小写的方式搜索信息,但首先您必须添加另一个函数,如下例所示:
>>> for doc in collection.find({'Location.Owner' : re.compile('^anderson. ', re.IGNORECASE)},
... fields={'ItemNumber' : True, 'Location.Owner' : True}):
... doc
...
{
u'ItemNumber': u'1234EXD',
u'_id': ObjectId('4c57207b4abffe0e0c000000'),
u'Location': {
u'Owner': u'Anderson, Thomas'
}
}
只要使用得当,正则表达式可能是一个非常强大的工具。关于re模块如何工作及其包含的功能的更多细节,请参考 http://docs.python.org/library/re.html 的模块官方文档。
修改数据
到目前为止,您已经学习了如何使用 Python 中的条件运算符和正则表达式来查询数据库中的信息。在下一节中,我们将研究如何使用 Python 来修改集合中的现有数据。我们可以使用 Python 以几种不同的方式来完成这项任务。接下来的几节将在前面使用的查询操作符的基础上寻找与您的修改相匹配的文档。在一些情况下,您可能需要跳回到本章的前面部分来温习使用查询操作符的特定方面——但这是学习过程中的正常部分,它将巩固到目前为止所学的课程。
更新您的数据
您使用 Python 的update()函数的方式与您在 MongoDB shell 或 PHP 驱动程序中使用同名函数的方式没有太大区别。在这种情况下,您需要提供两个强制参数来更新您的数据:arg和doc。arg参数指定用于匹配文档的键/值信息,而doc参数包含更新的信息。您还可以提供几个可选参数来指定您选项。以下列表涵盖了 Python 更新信息的选项列表,包括它们的作用:
upsert(optional):如果设置为True,则执行一次上插。manipulate(可选):如果设置为True,表示在使用 SONManipulator 的所有实例执行更新之前,将对文档进行操作。更多信息,请参考子 _ 操纵器文档(http://api.mongodb.org/python/current/api/pymongo/son_manipulator.html)。check_keys(可选):如果设置为True,update()将检查文档中是否有任何键以受限字符“$”或“.”开头替换 arg 时。multi(可选):如果设置为True,则更新任何匹配的文档,而不仅仅是它找到的第一个文档(默认操作)。建议您总是将此设置为True或False,而不是依赖默认行为(这在将来总是会改变)。w (optional):如果设置为 0,更新操作将不被确认。使用副本集时,w也可以设置为 n,确保主服务器在成功复制到 n 个节点时确认更新操作。也可以设置为majority—一个保留字符串,以确保大多数副本节点将确认更新,或者设置为特定的标记,以确保标记的节点将确认更新。该选项默认为 1,表示确认更新操作。wtimeout (optional):用于指定服务器等待接收确认的时间(毫秒)。默认为 10000。j (optional):如果设置为True,该布尔选项将在指示更新成功之前强制将数据写入日志。默认为False。fsync (optional):如果设置为True,该布尔选项会在返回成功之前将数据同步到磁盘。如果该选项被设置为True,则意味着w被设置为0,即使它没有被设置。
如果在更新文档时没有指定任何修改符操作符,那么默认情况下,文档中的所有信息将被替换为您在doc参数中插入的任何数据。最好避免依赖默认行为;相反,您应该使用前面提到的操作符来显式地指定您想要的更新(您将很快学习如何做到这一点)。
通过查看一个不使用任何条件操作符的例子,您可以明白为什么最好在update()命令中使用条件操作符:
// Define the updated data
>>> update = ( {
"Type" : "Chair",
"Status" : "In use",
"Tags" : ["Chair","In use","Marketing"],
"ItemNumber" : "6789SID",
"Location" : {
"Department" : "Marketing",
"Building" : "2B",
"DeskNumber" : 131131,
"Owner" : "Martin, Lisa"
}
} )
// Now, perform the update
>>> collection.update({"ItemNumber" : "6789SID"}, update)
// Inspect the result of the update
>>> collection.find_one({"Type" : "Chair"})
{
u'Status': u'In use',
u'Tags': [u'Chair', u'In use', u'Marketing'],
u'ItemNumber': u'6789SID',
u'Location': {
u'Department': u'Marketing',
u'Building': u'2B',
u'DeskNumber': 131131,
u'Owner': u'Martin, Lisa'
},
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Type': u'Chair'
}
这个例子的一个大缺点是它有点长,并且只更新了几个字段。接下来,我们将看看修饰符操作符可以用来完成什么。
修饰运算符
第 4 章详细介绍了 MongoDB shell 如何包含大量的修饰符操作符,您可以使用这些操作符更容易地操作数据,而无需重写整个文档来更改单个字段的值(正如您在前面的示例中所做的那样)。
修饰运算符允许您做任何事情,从更改文档中的一个现有值,到插入整个数组,到从数组中指定的多个项目中删除所有条目。作为一个组,这些操作符使得修改数据变得容易。现在让我们来看看操作符是做什么的,以及如何使用它们。
用$inc 增加一个整数值
您使用$inc运算符将文档中的整数值增加给定的数字 n。以下示例显示了如何将Location.Desknumber的整数值增加 20:
>>> collection.update({"ItemNumber" : "6789SID"}, {"$inc" : {"Location.DeskNumber" : 20}})
接下来,检查更新是否按预期工作:
>>> collection.find_one({"Type" : "Chair"}, fields={"Location" : "True"})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Location': {
u'Department': u'Marketing',
u'Building': u'2B',
u'Owner': u'Martin, Lisa',
u'DeskNumber': 131151
}
}
注意,$inc操作符只作用于整数值(即数值),而不能作用于任何字符串值,甚至不能作用于作为字符串添加的数值(例如,"123" vs. 123)。
用$set 更改现有值
您可以使用$set操作符来更改任何匹配文档中的现有值。这是一个您会经常使用的运算符。下一个示例更改当前匹配键/值"Location.Department / Development"的任何项目中"Building"的值。
首先使用$set执行更新,确保所有文档都已更新:
>>> collection.update({"Location.Department" : "Development"},
... {"$set" : {"Location.Building" : "3B"} },
... multi = True )
接下来,使用find_one()命令确认一切顺利:
>>> collection.find_one({"Location.Department" : "Development"}, fields={"Location.Building" : True})
{
u'_id': ObjectId('4c57207b4abffe0e0c000000'),
u'Location': {u'Building': u'3B'}
}
删除未设置$的键/值字段
同样,使用$unset操作符从文档中删除一个键/值字段,如下例所示:
>>> collection.update({"Status" : "Not used", "ItemNumber" : "2345FDX"},
... {"$unset" : {"Location.Building" : 1 } } )
接下来,使用find_one()命令确认一切顺利:
>>> collection.find_one({"Status" : "Not used", "ItemNumber" : "2345FDX"}, fields={"Location" : True})
{
u'_id': ObjectId('4c592eb84abffe0e0c000004'),
u'Location': {u'Department': u'Storage'}
}
用$push 向数组中添加值
假设数组存在,$push操作符允许您向数组添加一个值。如果数组不存在,将使用指定的值创建数组。
Warning
如果您使用$push来更新一个不是数组的现有字段,将会出现一个错误消息。
现在,您已经准备好向一个已经存在的数组添加一个值,并确认是否一切顺利。首先,执行更新:
>>> collection.update({"Location.Owner" : "Anderson, Thomas"},
... {"$push" : {"Tags" : "Anderson"} }, multi = True )
现在,执行find_one()来确认更新是否顺利:
>>> collection.find_one({"Location.Owner" : "Anderson, Thomas"}, fields={"Tags" : "True"})
{
u'_id': ObjectId('4c57207b4abffe0e0c000000'),
u'Tags': [u'Laptop', u'Development', u'In Use', u'Anderson']
}
用\(push 和\)each 将多个值添加到数组中
$push操作符也可以用来将多个值一次添加到一个现有数组中。这可以通过添加$each修改器来实现。这里,同样的规则也适用:数组必须已经存在,否则您将收到一个错误。以下示例结合使用$each修饰符和$正则表达式来执行搜索;这使您能够将更改应用于所有匹配的查询:
>>> collection.update({ "Location.Owner" : re.compile("^Walker,") },
... { '$push' : { 'Tags' : { '$each' : ['Walker','Warranty'] } } } )
接下来,执行find_one()来查看是否一切顺利:
>>> collection.find_one({"Location.Owner" : re.compile("^Walker,")}, fields={"Tags" : True})
{
u'_id': ObjectId('4c57234c4abffe0e0c000002'),
u'Tags': [u'Laptop', u'Development', u'In Use', u'Walker', u'Warranty']
}
用$addToSet 向现有数组添加值
$addToSet操作符还允许您向现有数组添加一个值。不同之处在于,这个方法在尝试更新之前检查数组是否已经存在(操作符$push不检查这个条件)。
该运算符只接受一个附加值;然而,知道您可以将$addToSet与$each操作符结合使用也很好。我们来看两个例子。首先,让我们使用$addToSet操作符对任何匹配"Type : Chair"的对象执行更新,然后使用find_one()函数检查是否一切顺利:
>>> collection.update({"Type" : "Chair"}, {"$addToSet" : {"Tags" : "Warranty"} }, multi = True)
>>> collection.find_one({"Type" : "Chair"}, {"Tags" : "True"})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Tags': [u'Chair', u'In use', u'Marketing', u'Warranty']
}
您也可以使用$each语句来添加多个标签。请注意,您使用正则表达式来执行此搜索。此外,列表中的一个标签已经被预先添加;好在不会再加了,因为这是$addToSet专门阻止的:
// Use the $each operator to add multiple tags, including one that was already added
>>> collection.update({"Type" : "Chair", "Location.Owner" : re.compile("^Martin,")},
... {"$addToSet" : { "Tags" : {"$each" : ["Martin","Warranty","Chair","In use"] } } } )
现在是检查一切是否顺利的时候了;具体来说,您希望验证重复的Warranty标记没有被再次添加:
>>> collection.find_one({"Type" : "Chair", "Location.Owner" : re.compile("^Martin,")}, fields={"Tags" : True})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Tags': [u'Chair', u'In use', u'Marketing', u'Warranty', u'Martin']
}
用$pop 从数组中移除元素
到目前为止,您已经看到了如何使用update()函数向现有文档添加值。现在让我们反过来看看如何删除数据。我们将从$pop操作符开始。
该运算符允许您删除数组中的第一个或最后一个值,但不能删除这两个值之间的任何内容。以下示例从找到的第一个符合"Type" : "Chair"条件的文档中删除Tags数组中的第一个值;然后,该示例使用find_one()命令来确认更新是否一切顺利:
>>> collection.update({"Type" : "Chair"}, {"$pop" : {"Tags" : -1}})
>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Tags': [u'In use', u'Marketing', u'Warranty', u'Martin']
}
相反,给Tags数组一个正值会删除数组中的最后一个匹配项,如下例所示:
>>> collection.update({"Type" : "Chair"}, {"$pop" : {"Tags" : 1}})
接下来,再次执行find_one()功能以确认一切顺利:
>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Tags': [u'In use', u'Marketing', u'Warranty']
}
使用$pull 删除特定值
$pull操作符允许您从数组中删除特定值的每次出现,而不管该值出现了多少次;只要值不变,就会被删除。
我们来看一个例子。首先使用$push操作符将值为Double的相同标签添加到Tags数组中:
>>> collection.update({"Type" : "Chair"}, {"$push" : {"Tags" : "Double"} }, multi = False )
>>> collection.update({"Type" : "Chair"}, {"$push" : {"Tags" : "Double"} }, multi = False )
接下来,通过执行find_one()命令,确保标记被添加了两次。一旦确认标签存在两次,使用$pull操作符删除标签的两个实例:
>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Tags': [u'In use', u'Marketing', u'Warranty', u'Double', u'Double']
}
>>> collection.update({"Type" : "Chair"}, {"$pull" : {"Tags" : "Double"} }, multi = False)
为了确认一切顺利,再次执行find_one()命令,这次确保结果不再列出Double标签:
>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Tags': [u'In use', u'Marketing', u'Warranty']
}
您可以使用$pullAll操作符来执行相同的操作;与$pull不同的是$pullAll让你移除多个标签。再来看一个例子。首先,您需要再次将多个项目添加到Tags数组中,并确认它们已经被添加:
>>> collection.update({"Type" : "Chair"}, {"$addToSet" : { "Tags" : {"$each" : ["Bacon","Spam"] } } } )
>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Tags': [u'In use', u'Marketing', u'Warranty', u'Bacon', u'Spam']
}
现在您可以使用$pullAll操作符来删除多个标签。下面的示例显示了如何使用此运算符;该示例还在之后立即执行一个find_one()命令,以确认Bacon和Spam标签已经被删除:
>>> collection.update({"Type" : "Chair"}, {"$pullAll" : {"Tags" : ["Bacon","Spam"] } }, multi = False)
>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : "True"})
{
u'_id': ObjectId('4c5973554abffe0e0c000005'),
u'Tags': [u'In use', u'Marketing', u'Warranty']
}
使用 save()快速保存文档
您可以使用save()功能通过 upsert 方法快速添加文档。为此,您还必须定义_id字段的值。如果您要保存的文档已经存在,它将被更新;如果它不存在,将被创建。
让我们看一个保存名为Desktop的文档的例子。首先通过在 shell 中键入一个标识符来指定文档,然后可以用save()函数保存它。一旦保存成功,执行save()函数将从文档中返回ObjectId:
>>> Desktop = ( {
"Status" : "In use",
"Tags" : ["Desktop","In use","Marketing","Warranty"],
"ItemNumber" : "4532FOO",
"Location" : {
"Department" : "Marketing",
"Building" : "2B",
"Desknumber" : 131131,
"Owner" : "Martin, Lisa",
}
} )
>>> collection.save(Desktop)
ObjectId('4c5ddbe24abffe0f34000001')
现在假设您意识到忘记在文档中指定一个键/值对。通过定义文档的名称,后跟方括号中的关键字,然后包括所需的内容,可以很容易地将这些信息添加到文档中。一旦你这样做了,你可以通过简单地再次保存整个文档来执行 upsert 这样做将再次从文档中返回ObjectId:
>>> Desktop[ "Type" ] = "Desktop"
>>> collection.save(Desktop)
ObjectId('4c5ddbe24abffe0f34000001')
如您所见,返回的ObjectId的值没有改变。
原子地修改文档
您可以使用find_and_modify()函数自动修改文档并返回结果。find_and_modify()函数只能用来更新一个文档——仅此而已。您还应该记住,默认情况下,返回的文档不会包含所做的修改;获取这些信息需要您指定一个附加参数,new: True。
find_and_modify()函数可以与多个参数一起使用,您必须包括update参数或remove参数。以下列表涵盖了所有可用的参数,解释了它们是什么以及它们的作用:
query:指定查询的过滤器。如果没有指定,那么集合中的所有文档都将被视为可能的候选文档,之后它遇到的第一个文档将被更新或删除。update:指定用于更新文档的信息。请注意,前面指定的任何修改操作符都可以用于此目的。upsert:如果设置为True,则执行一次上插。sort:按照指定的顺序对匹配的文档进行排序。full_response:如果设置为True,返回整个响应对象。remove:如果设置为True,则删除第一个匹配的文档。new:如果设置为True,则返回更新的文档,而不是选择的文档。然而,这不是默认设置,有时可能会有点混乱。fields:指定您希望看到返回的字段,而不是整个文档。这与find()功能的工作原理相同。注意,_id字段将总是被返回,除非被显式禁用。
让参数发挥作用
你知道参数的作用。现在是时候将它们与find_and_modify()函数结合使用了。首先使用find_and_modify()搜索任何具有"Type" : "Desktop"的键/值对的文档,然后通过设置一个额外的键/值对"Status" : "In repair"来更新每个匹配查询的文档。最后,您希望确保 MongoDB 返回更新的文档,而不是匹配查询的旧文档:
>>> collection.find_and_modify(query={"Type" : "Desktop"},
... update={'$set' : {'Status' : 'In repair'} }, new=True )
{
u'ok': 1.0,
u'value': {
u'Status': u'In repair',
u'Tags': [u'Desktop', u'In use', u'Marketing', u'Warranty'],
u'ItemNumber': u'4532FOO',
u'Location': {
u'Department': u'Marketing',
u'Building': u'2B',
u'Owner': u'Martin, Lisa',
u'Desknumber': 131131
},
u'_id': ObjectId('4c5dda114abffe0f34000000'),
u'Type': u'Desktop'
}
}
让我们看另一个例子。这一次,您将使用find_and_modify()删除一个文档;在这种情况下,输出将显示删除了哪个文档:
>>> collection.find_and_modify(query={'Type' : 'Desktop'},
... sort={'ItemNumber' : -1}, remove=True) {
u'ok': 1.0,
u'value': {
u'Status': u'In use',
u'Tags': [u'Desktop', u'In use', u'Marketing', u'Warranty'],
u'ItemNumber': u'4532FOO',
u'Location': {
u'Department': u'Marketing',
u'Building': u'2B',
u'Owner': u'Martin, Lisa',
u'Desknumber': 131131
},
u'_id': ObjectId('4c5ddbe24abffe0f34000001'),
u'Type': u'Desktop'
}
}
删除数据
大多数情况下,您将使用 Python 驱动程序来添加或修改数据。然而,了解如何删除数据也很重要。Python 驱动程序提供了几种删除数据的方法。首先,您可以使用remove()函数从集合中删除单个文档。其次,您可以使用drop()或drop_collection()功能删除整个收藏。最后,您可以使用drop_database()函数删除整个数据库。(看起来你不太可能经常使用这个功能!)
然而,我们将更仔细地看看这些函数,看看所有这些函数的例子。
让我们从查看remove()函数开始。此函数允许您指定一个参数作为参数,用于在当前集合中查找和删除任何匹配的文档。在这个例子中,您使用remove()函数删除每个具有"Status" : "In use"的键/值对的文档;之后,使用find_one()功能确认结果:
>>> collection.remove({"Status" : "In use"})
>>> collection.find_one({"Status" : "In use"})
>>>
您需要小心使用这个函数指定什么样的标准。通常,您应该首先执行一个find(),这样您就可以确切地看到哪些文档将被删除。或者,您可以使用ObjectId删除一个项目。
如果您厌倦了整个集合,您可以使用drop()或drop_collection()函数来删除它。两个函数的工作方式相同(实际上,一个只是另一个的别名);具体来说,两者都只需要一个参数,即集合的名称:
>>> db.items.drop()
最后(因为其潜在的破坏性),函数drop_database()使您能够删除整个数据库。您可以使用Connection模块调用这个函数,如下例所示:
>>> c.drop_database("inventory")
在两个文档之间创建链接
数据库引用(DBRefs)是在 PyMongo 中通过 DBRef()函数实现的,它是 DBRef 模块的一部分,可用于在位于不同位置的两个文档之间创建链接。这是一个字段存储约定,可用于应用逻辑。例如,您可以为所有雇员创建一个集合,为所有项目创建另一个集合,然后使用DBRef()函数创建雇员和项目位置之间的引用,而不是为每个项目手动键入它们。
Note
MongoDB 无法确保 DBRefs 有效。因此,在删除可能由 DBRefs 链接的文档时应小心谨慎。
您可能还记得前面的章节,您可以通过两种方式引用数据。首先,您可以添加一个简单的引用(手动引用),它使用一个文档中的_id字段来存储另一个文档中对它的引用。第二,您可以使用DBRef模块,它带来了比手动引用更多的选项。
让我们先创建一个手动引用。从保存文档开始。例如,假设您想要将某个人的信息保存到特定的集合中。下面的例子定义了一个jan字典,并将其保存到people集合中,以获取一个ObjectId:
>>> jan = {
... "First Name" : "Jan",
... "Last Name" : "Walker",
... "Display Name" : "Walker, Jan",
... "Department" : "Development",
... "Building" : "2B",
... "Floor" : 12,
... "Desk" : 120103,
... "E-Mail" : "``jw@example.com/
... }
>>> people = db.people
>>> people.insert(jan)
ObjectId('4c5e5f104abffe0f34000002')
添加项目并取回其 ID 后,可以使用此信息将项目链接到另一个集合中的另一个文档:
>>> laptop = {
... "Type" : "Laptop",
... "Status" : "In use",
... "ItemNumber" : "12345ABC",
... "Tags" : ["Warranty","In use","Laptop"],
... "Owner" : jan[ "_id" ]
... }
>>> items = db.items
>>> items.insert(laptop)
ObjectId('4c5e6f6b4abffe0f34000003')
现在假设你想找出主人的信息。在这种情况下,您所要做的就是查询在Owner字段中给出的ObjectId;显然,只有当您知道数据存储在哪个集合中时,这才是可能的。
但是假设你不知道这些信息存储在哪里。正是为了处理这样的场景,创建了DBRef()函数。即使您不知道哪个集合包含原始数据,也可以使用这个函数,这样在搜索信息时就不必担心集合名称了。
DBRef()函数有三个参数;它可以接受第四个参数,您可以用它来指定附加的关键字参数。以下是三个主要论点的列表,以及它们能让你做什么:
collection(必填):指定原始数据所在的集合(例如people)。id(必填):指定需要引用的单据的_id值。database(可选):指定要引用的数据库的名称。
在使用DBRef函数之前必须加载DBRef模块,所以让我们在进一步操作之前加载模块:
>>> from bson.dbref import DBRef
至此,您已经准备好查看一个DBRef()函数的实际例子。在下面的示例中,您将一个人插入到people集合中,并将一个项目添加到items集合中,使用DBRef来引用所有者:
>>> mike = {
... "First Name" : "Mike",
... "Last Name" : "Wazowski",
... "Display Name" : "Wazowski, Mike",
... "Department" : "Entertainment",
... "Building" : "2B",
... "Floor" : 10,
... "Desk" : 120789,
... "E-Mail" : "mw@monsters.inc"
... }
>>> people.save(mike)
ObjectId('4c5e73714abffe0f34000004')
在这一点上,没有发生任何有趣的事情。是的,您添加了一个文档,但是您没有添加对它的引用。但是,您确实有文档的ObjectId,所以现在您可以将下一个文档添加到集合中,然后使用DBRef()将owner字段指向之前插入的文档的值。特别注意DBRef()函数的语法;特别是,您应该注意到给出的第一个参数是您先前指定的文档所在的集合的名称,而第二个参数只不过是对mike字典中的_id键的引用:
>>> laptop = {
... "Type" : "Laptop",
... "Status" : "In use",
... "ItemNumber" : "2345DEF",
... "Tags" : ["Warranty","In use","Laptop"],
... "Owner" : DBRef('people', mike[ "_id" ])
... }
>>> items.save(laptop)
ObjectId('4c5e740a4abffe0f34000005')
您可能已经注意到,这段代码与您用来创建手动引用的代码没有太大的不同。但是,我们建议您在需要引用特定信息时使用 DBRefs,而不是嵌入它。采用这种方法还意味着无论何时查询引用的信息,都不需要查找集合的名称。
检索信息
你知道如何用DBRef()引用信息;现在让我们假设您想要检索前面引用的信息。您可以使用 Python 驱动程序的dereference()函数来实现这一点。您所需要做的就是将先前指定的包含引用信息的字段定义为一个参数,然后按 Return 键。
为了进行演示,让我们从头到尾看一遍从一个文档到另一个文档引用和检索信息的过程。让我们首先找到包含引用数据的文档,然后检索该文档进行显示。第一步是创建一个查询,查找包含引用信息的随机文档:
>>> items.find_one({"ItemNumber" : "2345DEF"})
{
u'Status': u'In use',
u'Tags': [u'Warranty', u'In use', u'Laptop'],
u'ItemNumber': u'2345DEF',
u'Owner': DBRef(u'people', ObjectId('4c5e73714abffe0f34000004')),
u'_id': ObjectId('4c5e740a4abffe0f34000005'),
u'Type': u'Laptop'
}
接下来,您希望将此项存储在一个person字典下:
>>> person = items.find_one({"ItemNumber" : "2345DEF"})
此时,您可以使用dereference()函数将Owner字段解引用到person["Owner"]字段作为参数。这是可能的,因为Owner字段链接到您想要检索的数据:
>>> db.dereference(person["Owner"])
{
u'Building': u'2B',
u'Floor': 10,
u'Last Name': u'Wazowski',
u'Desk': 120789,
u'E-Mail': u'mw@monsters.inc',
u'First Name': u'Mike',
u'Display Name': u'Wazowski, Mike',
u'Department': u'Entertainment',
u'_id': ObjectId('4c5e73714abffe0f34000004')
}
那还不算太糟!这个例子要说明的一点是,DBRefs 技术为存储您想要引用的数据提供了一种很好的方式。此外,它允许在指定集合和数据库名称时有一定的灵活性。如果您想保持数据库整洁,您会发现自己经常使用这个特性,尤其是在数据确实不应该嵌入的情况下。
摘要
在这一章中,我们探索了如何将 MongoDB 的 Python 驱动程序(PyMongo)用于最常用的操作。一路上,我们讲述了如何搜索、存储、更新和删除数据。
我们还研究了如何使用两种方法引用另一个集合中包含的文档:手动引用和 DBRefs。当观察这些方法时,我们已经看到它们的语法非常相似,但是 DBRefs 方法在功能方面提供了更多的健壮性,所以在大多数情况下它是更好的。
下一章将深入探讨 MongoDB 更高级的查询方法。
八、高级查询
Abstract
到目前为止,这几章已经介绍了根据给定的标准查找一个或一系列文档的大多数基本查询机制。有许多机制可以找到给定的文档,并将它们返回到您的应用中,以便进行处理。但是有时这些普通的查询机制不能满足需要,您希望对集合中的大多数或所有文档执行复杂的操作。当需要这种查询或操作时,许多开发者要么遍历集合中的所有文档,要么编写一系列要按顺序执行的查询来执行必要的计算。虽然这是一种有效的做事方式,但是编写和维护起来很麻烦,而且效率很低。正是由于这些原因,MongoDB 提供了一些高级的查询机制,您可以使用它们来最大限度地利用您的数据。我们将在本章研究的高级 MongoDB 特性是全文搜索、聚合框架和 MapReduce 框架。
到目前为止,这几章已经介绍了根据给定的标准查找一个或一系列文档的大多数基本查询机制。有许多机制可以找到给定的文档,并将它们返回到您的应用中,以便进行处理。但是有时这些普通的查询机制不能满足需要,您希望对集合中的大多数或所有文档执行复杂的操作。当需要这种查询或操作时,许多开发者要么遍历集合中的所有文档,要么编写一系列要按顺序执行的查询来执行必要的计算。虽然这是一种有效的做事方式,但是编写和维护起来很麻烦,而且效率很低。正是由于这些原因,MongoDB 提供了一些高级的查询机制,您可以使用它们来最大限度地利用您的数据。我们将在本章研究的高级 MongoDB 特性是全文搜索、聚合框架和 MapReduce 框架。
全文搜索是 MongoDB 中最受欢迎的特性之一。它表示能够在 MongoDB 中创建专门的文本索引,然后在这些索引上执行文本搜索,以定位包含匹配文本元素的文档。MongoDB 全文搜索特性不仅仅是简单的字符串匹配,还包括基于您为文档选择的语言的全词干方法,这是一个非常强大的工具,用于对文档执行语言查询。这个最近引入的特性在 MongoDB 的 2.4 版本中被标记为“实验性的”,因为开发团队仍在努力改进它,这意味着您必须手动激活它才能在您的 MongoDB 环境中使用。
本章将介绍的第二个特性是 MongoDB 聚合框架。在第 4 章和第 6 章中介绍了这个特性,它提供了一整套查询特性,可以让你迭代选择的文档,或者所有的文档,收集或者操作信息。然后,这些查询函数被安排到一个操作管道中,在您的集合上一个接一个地执行这些操作,以从您的查询中收集信息。
我们将讨论的第三个也是最后一个特性叫做 MapReduce,对于那些使用过 Hadoop 的人来说,这个特性听起来很熟悉。MapReduce 是一种强大的机制,它利用 MongoDB 内置的 JavaScript 引擎来实时执行抽象代码。这是一个非常强大的工具,它使用了两个 JavaScript 函数,一个用于映射数据,另一个用于从映射的数据中转换和提取信息。
在本章中要记住的最重要的事情可能是,这些是真正的高级特性,如果它们被误用,可能会给 MongoDB 节点带来严重的性能问题,所以只要有可能,在将它们部署到重要系统之前,应该在测试环境中测试这些特性。
文本搜索
MongoDB 的文本搜索首先创建一个全文索引,并指定您希望被索引的字段,以方便文本搜索。这个文本索引将检查集合中的每个文档,并对每个文本字符串进行标记和词干处理。这个记号化和词干化的过程包括将文本分解成记号,这些记号在概念上接近单词。然后,MongoDB 对每个令牌进行词干分析,以找到该令牌的根概念。例如,假设分解一个字符串到达 token fishing。然后这个标记被追溯到词根 fish,因此 MongoDB 为该文档创建了一个 fish 索引条目。同样的标记化和词干化过程也适用于用户输入的搜索参数,以执行给定的文本搜索。然后将这些参数与每个文档进行比较,并计算相关性分数。然后根据用户的分数将文档返回给用户。
您可能想知道如何对像 the 或 it 这样的单词进行词干处理,如果文档不是英文的会怎么样。答案是这些词和类似的词不会被词干化,MongoDB 文本搜索支持许多语言。
MongoDB 文本搜索引擎是为 MongoDB,Inc .团队编写的用于文本数据检索的专有引擎。MongoDB 文本搜索还利用了 Snowball 字符串处理语言,该语言支持词干提取和停用词,即那些不需要词干提取的词,因为它们在索引或搜索方面不代表任何有价值的概念。
从这一点来看,MongoDB 的文本搜索非常复杂,并且设计得尽可能灵活和准确。
文本搜索的成本和限制
从您所了解的文本搜索功能可以想象,使用 MongoDB 文本搜索会有一些相关的成本。首先,它将未来文档的文档存储分配更改为 usePowerOf2Sizes 选项,该选项指示 MongoDB 分配存储以更有效地重用空闲空间。第二,文本索引很大,根据您存储的文档数量和每个索引字段中的标记数量,它会增长得非常快。第三个限制是,在现有文档上构建文本索引非常耗时,并且需要向具有文本索引的字段添加新条目,这也是成本较高的。第四,像 MongoDB 中的所有东西一样,文本索引在 RAM 中工作得更好。最后,由于文本索引的复杂性和大小,它们目前被限制为每个集合一个。
启用文本搜索
如前所述,文本搜索是在 MongoDB 2.4 中作为一个实验性或测试版特性引入的。因此,您需要在集群中使用这个特性的每个 MongoDB 实例(如果是分片的话,还有 MongoS)上显式启用文本搜索功能。有三种方法可以启用文本搜索;第一种方法是在用于启动或停止 MongoDB 进程的命令中添加以下选项:
--setParameter textSearchEnabled=true
第二种方法是在 MongoDB 实例的配置文件中添加以下选项:
setParameter = textSearchEnabled=true
让文本搜索在 MongoDB 实例上工作的第三种也是最后一种方法是通过 Mongo shell 运行以下命令:
db.adminCommand({ setParameter: 1, textSearchEnabled : true }
有了这个集合,您现在可以在这个节点上使用 MongoDB 全文搜索特性。
Note
这一功能处于测试阶段并不意味着它不起作用。MongoDB,Inc .团队已经付出了相当大的努力来获得这个特性。通过使用该功能并报告您在 MongoDB,Inc. JIRA (jira.mongodb.org)上遇到的任何问题,您可以帮助他们准备好该功能的正式发布。
到目前为止,您应该已经在 MongoDB 实例上启用了文本搜索特性,并准备好利用它了!让我们看看如何创建文本搜索索引和执行文本搜索。
使用文本搜索
尽管我们已经描述了所有的复杂性,MongoDB 文本搜索非常容易使用;创建文本索引的方式与创建任何其他索引的方式相同。例如,为了在我们的理论博客集合的“内容”元素上创建一个文本索引,我将运行以下代码
db.blog.ensureIndex( { content : "text" } );
仅此而已。MongoDB 将处理剩下的工作,并在您的数据库中插入一个文本索引,所有将来有内容字段的文档都将被处理,并将条目添加到要搜索的文本索引中。但是实际上,仅仅创建一个索引是不够的。我们需要一组合适的文本数据来处理和查询。
加载文本数据
最初,我们计划使用来自 twitter 的实时数据流,但这些文档太难看了,无法使用。因此,我们创建了一个由八个文档组成的小批量文件,模仿 twitter feeds,让文本搜索变得简单起来。
继续将来自twitter.tgz的 MongoDB 数据mongoimport到您的数据库中:
$ mongoimport test.json -d test -c texttest
connected to: 127.0.0.1
Sat Jul 6 17:52:19 imported 8 objects
现在我们已经恢复了数据,如果还没有启用文本索引,请继续:
db.adminCommand({ setParameter: 1, textSearchEnabled : true });
{ "was" : false, "ok" : 1 }
既然我们已经启用了文本索引,我们应该在 twitter 数据上创建一个文本索引。
创建文本索引
对于 twitter 数据,我们关心的部分是text字段,它是推文的正文。要设置文本索引,我们运行以下命令:
use test;
db. texttest.ensureIndex( { body : "text" } );
如果您看到错误消息“text search not enabled”,您需要使用刚才显示的命令来确保文本索引正在运行。现在,如果您查看您的日志,您将看到以下内容,其中显示了正在构建的文本索引:
Sat Jul 6 17:54:16.078 [conn41] build index test.texttest { _fts: "text", _ftsx: 1 }
Sat Jul 6 17:54:16.089 [conn41] build index done. scanned 8 total records. 0.01 secs
我们还可以检查集合的索引:
db.texttest.getIndexes()
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"ns" : "test.texttest",
"name" : "_id_"
},
{
"v" : 1,
"key" : {
"_fts" : "text",
"_ftsx" : 1
},
"ns" : "test.texttest",
"name" : "body_text",
"weights" : {
"body" : 1
},
"default_language" : "english",
"language_override" : "language",
"textIndexVersion" : 1
}
]
好了,我们已经启用了文本搜索,创建了我们的索引,并确认它在那里;现在让我们运行文本搜索命令。
运行文本搜索命令
在我们使用的 MongoDB 版本中,text命令没有 shell 助手,所以我们使用如下的runCommand语法来执行它:
> db.texttest.runCommand( "text", { search :"fish" } )
该命令将返回任何匹配查询字符串"fish"的文档。在这种情况下,它返回了两个文档。输出显示了相当多的调试信息,以及一个"results"数组;这包括许多文件。这些包括匹配文档和返回的得分的组合,匹配文档作为obj。您可以看到匹配文档的文本部分都包含单词 fish 或 fishing,它们都与我们的查询匹配!同样值得注意的是,MongoDB 文本索引是不区分大小写的,这是执行文本查询时的一个重要考虑因素。
Note
请记住,文本搜索中的所有条目都被标记化和词干化。这意味着像 fishy 或 fishing 这样的词将被归结为 fish。
此外,您可以看到分数,0.75 或 0.666,表明该结果与您的查询的相关性—值越高,匹配越好。您还可以看到查询的统计数据,包括返回的对象数量(2)和花费的时间(112 微秒)。
{
"queryDebugString" : "fish||||||",
"language" : "english",
"results" : [
{
"score" : 0.75,
"obj" : {
"_id" : ObjectId("51d7ccb36bc6f959debe5514"),
"number" : 1,
"body" : "i like fish",
"about" : "food"
}
},
{
"score" : 0.6666666666666666,
"obj" : {
"_id" : ObjectId("51d7ccb36bc6f959debe5516"),
"number" : 3,
"body" : "i like to go fishing",
"about" : "recreation"
}
}
],
"stats" : {
"nscanned" : 2,
"nscannedObjects" : 0,
"n" : 2,
"nfound" : 2,
"timeMicros" : 112
},
"ok" : 1
}
现在让我们来看看其他一些可以用来增强文本查询的文本搜索特性。
过滤文本查询
我们可以做的第一件事是过滤文本查询。为了细化我们的鱼查询,假设我们只想要引用鱼作为食物的文档,而不是任何匹配“钓鱼”活动的文档。为了添加这个额外的参数,我们使用filter选项并提供一个带有普通查询的文档。因此,为了找到我们的鱼作为食物,我们运行以下:
> db.texttest.runCommand( "text", { search : "fish", filter : { about : "food" } })
{
"queryDebugString" : "fish||||||",
"language" : "english",
"results" : [
{
"score" : 0.75,
"obj" : {
"_id" : ObjectId("51d7ccb36bc6f959debe5514"),
"number" : 1,
"body" : "i like fish",
"about" : "food"
}
}
],
"stats" : {
"nscanned" : 2,
"nscannedObjects" : 2,
"n" : 1,
"nfound" : 1,
"timeMicros" : 101
},
"ok" : 1
}
那太完美了;我们只返回了我们想要的一个项目,没有得到不相关的“钓鱼”文档。注意,nScanned和nscannedObjects的值是 2,这表示该查询从索引中扫描了两个文档(nScanned),然后必须检索这两个文档来检查它们的内容(nScannedObjects)以返回一个匹配的文档(n)。现在我们来看另一个例子。
更复杂的文本搜索
首先运行下面的查询,它将返回两个文档。为了简洁起见,结果被削减到只有文本字段。
db.texttest.runCommand( "text", { search : "cook" })
"body" : "i want to cook dinner",
"body" : "i am to cooking lunch",
如你所见,我们有两份文件,都是关于做饭的。假设我们想从搜索中排除午餐,只返回晚餐。我们可以通过添加–lunch来将文本lunch从我们的搜索中排除。
> db.texttest.runCommand( "text", { search : "cook -lunch" })
{
"queryDebugString" : "cook||lunch||||",
"language" : "english",
"results" : [
{
"score" : 0.6666666666666666,
"obj" : {
"_id" : ObjectId("51d7ccb36bc6f959debe5518"),
"number" : 5,
"body" : "i want to cook dinner",
"about" : "activities"
}
}
],
"stats" : {
"nscanned" : 2,
"nscannedObjects" : 0,
"n" : 1,
"nfound" : 1,
"timeMicros" : 150
},
"ok" : 1
}
首先请注意,queryDebugString包含了cook和lunch,因为这是我们正在使用的搜索词。还要注意,扫描了两个条目,但只返回了一个。搜索的工作方式是首先找到所有匹配项,然后排除不匹配项。
人们可能会发现最后一个有价值的搜索功能是字符串搜索,它可以用来匹配特定的单词或短语,而无需词干。目前,我们个人搜索的所有元素都被标记化,然后被词干化,每个词都被评估。以下面的查询为例:
> db.texttest.runCommand( "text", { search : "mongodb text search" })
{
"queryDebugString" : "mongodb|search|text||||||",
"language" : "english",
"results" : [
{
"score" : 3.875,
"obj" : {
"_id" : ObjectId("51d7ccb36bc6f959debe551a"),
"number" : 7,
"body" : "i like mongodb text search",
"about" : "food"
}
},
{
"score" : 3.8000000000000003,
"obj" : {
"_id" : ObjectId("51d7ccb36bc6f959debe551b"),
"number" : 8,
"body" : "mongodb has a new text search feature",
"about" : "food"
}
}
],
"stats" : {
"nscanned" : 6,
"nscannedObjects" : 0,
"n" : 2,
"nfound" : 2,
"timeMicros" : 537
},
"ok" : 1
}
您可以在queryDebugString中看到每个元素都被评估和查询。您还可以看到,该查询评估并找到了两个文档。现在,请注意当我们运行带有转义引号的相同查询以使其成为字符串文字时的区别:
> db.texttest.runCommand( "text", { search : "\"mongodb text search\"" })
{
"queryDebugString" : "mongodb|search|text||||mongodb text search||",
"language" : "english",
"results" : [
{
"score" : 3.875,
"obj" : {
"_id" : ObjectId("51d7ccb36bc6f959debe551a"),
"number" : 7,
"body" : "i like mongodb text search",
"about" : "food"
}
}
],
"stats" : {
"nscanned" : 6,
"nscannedObjects" : 0,
"n" : 1,
"nfound" : 1,
"timeMicros" : 134
},
"ok" : 1
}
您可以看到只返回了一个文档,这个文档实际上包含有问题的文本。您还可以看到,在queryDebugString中,最后一个元素是字符串本身,而不仅仅是三个标记化和词干化的元素。
附加选项
除了我们到目前为止已经讨论过的,还有三个选项可以添加到text函数中。第一个是limit,限制返回的文档数量。它可以按如下方式使用:
> db.texttest.runCommand( "text", { search :"fish", limit : 1 } )
第二个选项是project,它允许您设置将作为查询结果显示的字段。此选项获取一个描述您希望显示哪些字段的文档,0 表示关闭,1 表示打开。默认情况下,指定此选项时,除了_id打开之外,所有元素都关闭。
> db.texttest.runCommand( "text", { search :"fish", project : { _id : 0, body : 1 } } )
第三个也是最后一个选项是language,它允许您指定文本搜索将使用哪种语言。如果没有指定语言,则使用索引的默认语言。语言必须全部用小写字母指定。可以按如下方式调用它:
> db.texttest.runCommand( "text", { search :"fish", lagnuage : "french" } )
目前,文本搜索支持以下语言。
- 丹麦的
- 荷兰人
- 英语
- 芬兰人的
- 法语
- 德国人
- 匈牙利的
- 意大利的
- 挪威的
- 葡萄牙语
- 罗马尼亚的
- 俄语
- 西班牙语
- 瑞典的
- 土耳其的
有关 MongoDB 文本搜索目前支持的更多细节,请参见页面。mongodb。org/manual/reference/command/text/
其他语言的文本索引
我们最初在前面创建了一个简单的文本索引,以便开始我们的文本工作。但是,您可以使用许多其他技术来使您的文本索引更适合您的工作负载。您可能还记得,单词如何词干化的逻辑会根据 MongoDB 用来执行它的语言而改变。默认情况下,所有的索引都是用英语创建的,但是这并不适合很多人,因为他们的数据可能不是英语的,因此语言的规则是不同的。您可以在每个查询中指定要使用的语言,但是当您知道正在使用哪种语言时,这并不十分友好。您可以通过将该选项添加到索引创建中来指定默认语言:
db. texttest.ensureIndex( { content : "text" }, { default_language : "french" } );
这将创建一个以法语为默认语言的文本索引。现在请记住,每个集合只能有一个文本索引,因此在创建这个索引之前,需要删除任何其他索引。
但是如果我们在一个集合中有多种语言呢?文本索引特性提供了一个解决方案,但是它要求您用正确的语言标记所有文档。您可能认为由 MongoDB 来确定给定文档是哪种语言会更好,但是没有编程方式来进行精确的语言匹配。相反,MongoDB 允许您处理指定自己语言的文档。例如,以下面四个文档为例:
{ _id : 1, content : "cheese", lingvo : "english" }
{ _id : 2, content : "fromage", lingvo: "french" }
{ _id : 3, content : "queso", lingvo: "spanish" }
{ _id : 4, content : "ost", lingvo: "swedish" }
它们包括四种语言(在lingvo字段中),如果我们保留任何一种默认语言,那么我们需要指定我们将在其中搜索的语言。因为我们已经指定了给定内容的语言,所以我们可以将该字段用作语言覆盖,并且将使用给定的语言而不是默认语言。我们可以用它创建一个索引,如下所示:
db.texttest.ensureIndex( { content : "text" }, { language_override : "lingvo" } );
因此,这些文档的默认语言将是所提供的语言,任何缺少lingvo字段的文档都将使用默认索引,在本例中为英语。
带文本索引的复合索引
虽然在一个集合中只能有一个文本索引,但是可以让文本索引覆盖文档中多个字段,甚至所有字段。您可以像对普通索引一样指定额外的字段。假设我们想索引内容和任何评论;我们可以这样做。现在,我们可以在这两个字段中进行文本搜索。
db.texttest.ensureIndex( { content : "text", comments : "text" });
您甚至可能希望为文档中的所有字段创建文本索引。MongoDB 有一个通配符说明符,可以用来引用所有文档的所有文本元素;符号是"$**"。如果您希望将它指定为您的文本索引的形式,您将需要为您的文档添加名称的索引选项。这样,自动生成的名称将不会被使用,也不会因为字段太长而导致索引问题。索引的最大长度是 121 个字符,包括集合、数据库和要索引的字段的名称。
Note
强烈建议您为任何包含文本字段的复合索引指定一个名称,以避免名称长度导致的问题。
这为我们提供了以下语法,用于在texttest集合中所有文档的所有字符串元素上创建名为alltextindex的文本索引:
db.texttest.ensureIndex( { "$**": "text" }, { name: "alltextindex" } )
使用复合文本索引可以做的下一件事是为该索引上的不同文本字段指定权重。要做到这一点,可以向要索引的每个字段添加高于默认值 1 的权重值。然后,这些值将以 N:1 的比率增加给定索引结果的重要性。以下面的索引为例:
db.texttest.ensureIndex( { content : "text", comments : "text"}, { weights : { content: 10, comments: 5, } } );
这个索引意味着文档的content部分将比comments值优先 10:5 倍。任何其他字段的默认权重为 1,相比之下,评论的权重为 5,内容的权重为 10。您还可以结合权重和通配符文本搜索参数来对特定字段进行加权。
Note
请注意,如果您有太多的文本索引,您将得到一个“太多的文本索引”错误。如果发生这种情况,您应该删除一个现有的文本索引,以便创建新的文本索引。
除了使用其他文本字段创建复合索引之外,还可以使用其他非文本字段创建复合索引。您可以像添加任何其他索引一样构建这些索引,如下例所示:
db.texttest.ensureIndex( { content : "text", username : 1 });
该命令在文档的content部分创建一个文本索引,在username部分创建一个普通索引。这在使用filter参数时特别有用,因为过滤器实际上是对所有使用的子文档的查询。这些也需要从索引中或通过阅读文档本身来读取。让我们看看前面的例子:
db.texttest.runCommand( "text", { search : "fish", filter : { about : "food" } })
给定这个查询的过滤器,我们将需要索引文档的about部分;否则,每一个理论上匹配的文档都需要完全阅读,然后进行验证,这是一个代价高昂的过程。但是,如果我们按如下方式建立索引,我们可以通过这样的索引来避免这些读取,这样的索引包含了about元素:
db.texttest.ensureIndex( { about : 1, content : "text" });
现在让我们再次运行find命令:
> db.texttest.runCommand( "text", { search : "fish", filter : { about : "food" } })
{
"queryDebugString" : "fish||||||",
"language" : "english",
"results" : [
{
"score" : 0.75,
"obj" : {
"_id" : ObjectId("51d7ccb36bc6f959debe5514"),
"number" : 1,
"body" : "i like fish",
"about" : "food"
}
}
],
"stats" : {
"nscanned" : 1,
"nscannedObjects" : 0,
"n" : 1,
"nfound" : 1,
"timeMicros" : 95
},
"ok" : 1
}
您可以看到没有扫描的对象,这应该会提高查询的整体效率。有了这些选项,你应该能够为你的文本搜索带来真正的灵活性和力量。
现在,您应该已经看到了 MongoDB 最新搜索特性的巨大威力,并且应该已经掌握了从文本搜索中获得真正力量的知识。
聚合框架
MongoDB 中的聚合框架表示对集合中的所有数据执行选择操作的能力。这是通过创建一个聚合操作管道来实现的,这些操作将首先对数据按顺序执行,然后每个后续操作将对上一个操作的结果执行。熟悉 Linux 或 Unix shell 的人会认为这构成了操作的 shell 管道。
在聚合框架中有大量的操作符,它们可以作为聚合的一部分来收集数据。在这里,我们将介绍一些高级管道操作符,并通过一些例子来演示如何使用它们。这意味着我们将涵盖以下运营商:
$group$limit$match$sort$unwind$project$skip
关于全套操作符的更多细节,请查看聚合文档,可从 http://docs.mongodb.org/manual/aggregation/ 获得。我们已经创建了一个示例集合,您可以使用它来测试一些聚合命令。使用以下命令提取归档文件:
$ tar -xvf test.tgz
x test/
x test/aggregation.bson
x test/aggregation.metadata.json
x test/mapreduce.bson
x test/mapreduce.metadata.json
接下来要做的是运行mongorestore命令来恢复测试数据库:
$ mongorestore test
connected to: 127.0.0.1
Sun Jul 21 19:26:21.342 test/aggregation.bson
Sun Jul 21 19:26:21.342 going into namespace [test.aggregation]
1000 objects found
Sun Jul 21 19:26:21.350 Creating index: { key: { _id: 1 }, ns: "test.aggregation", name: "_id_" }
Sun Jul 21 19:26:21.688 test/mapreduce.bson
Sun Jul 21 19:26:21.689 going into namespace [test.mapreduce]
1000 objects found
Sun Jul 21 19:26:21.695 Creating index: { key: { _id: 1 }, ns: "test.mapreduce", name: "_id_" }
既然我们已经有了要处理的数据集合,我们需要看看如何运行聚合命令以及如何构建聚合管道。为了运行聚合查询,我们使用aggregate命令,并为它提供一个包含管道的文档。对于我们的测试,我们将使用各种管道文档运行以下聚合命令:
> db.aggregation.aggregate({pipeline document})
所以,事不宜迟,让我们开始研究我们的聚合示例。
$组
$group命令顾名思义就是这样做的;它将文档分组在一起,因此您可以创建结果的集合。让我们首先创建一个简单的 group 命令,它将列出我们的“aggregation”集合中所有不同的颜色。首先,我们创建一个_id文档,该文档将列出我们想要分组的集合中的所有元素。因此,我们用$group命令开始我们的管道文档,并向其中添加我们的_id文档:
{ $group : { _id : "$color" } }
现在你可以看到我们有了"$color"的_id值。注意color这个名字前面有一个$的标志;这表明该元素是对我们文档中某个字段的引用。这为我们提供了基本的文档结构,所以让我们执行聚合:
> db.aggregation.aggregate( { $group : { _id : "$color" } } )
{
"result" : [
{
"_id" : "red"
},
{
"_id" : "maroon"
},
...
{
"_id" : "grey"
},
{
"_id" : "blue"
}
],
"ok" : 1
}
美元总数
从$group操作符的结果中,你可以看到我们的结果堆栈中有许多不同的颜色。结果是一个元素数组,其中包含许多文档,每个文档的_id值是文档的"color"字段中的一种颜色。这实际上并没有告诉我们太多,所以让我们扩展一下我们使用$group命令所做的事情。我们可以使用$sum操作符为我们的组添加一个计数,它可以为找到的值的每个实例增加一个值。为此,我们通过为新字段提供一个名称以及它的值应该是什么来为我们的$group命令添加一个额外的值。在这种情况下,我们需要一个名为"count"的字段,因为它代表每种颜色出现的次数;它的值是{$sum : 1},这意味着我们希望为每个文档创建一个总和,并且每次增加 1。这为我们提供了以下文档:
{ $group : { _id : "$color", count : { $sum : 1 } }
让我们用这个新文档来运行我们的聚合:
> db.aggregation.aggregate({ $group : { _id : "$color", count : { $sum : 1 } } }
{
"result" : [
{
"_id" : "red",
"count" : 90
},
{
"_id" : "maroon",
"count" : 91
},
...
{
"_id" : "grey",
"count" : 91
},
{
"_id" : "blue",
"count" : 91
}
],
"ok" : 1
}
现在你可以看到每种颜色出现的频率。我们可以通过向_id文档添加额外的元素来进一步扩展我们正在分组的内容。假设我们想要找到由"color"和"transport"组成的组。为此,我们可以将_id更改为包含项目子文档的文档,如下所示:
{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } }
如果我们运行这个,我们会得到一个大约 50 个元素长的结果,太长了,无法在这里显示。对此有一个解决方案,那就是$limit操作符。
美元限额
$limit操作符是我们将合作的下一个管道操作符。顾名思义,$limit用于限制返回结果的数量。在我们的例子中,我们希望使现有管道的结果更易于管理,所以让我们给结果增加一个 5 的限制。为了增加这个限制,我们需要将我们的一个文档转换成一组管道文档。
[
{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } },
{ $limit : 5 }
]
这将为我们提供以下结果:
> db.aggregation.aggregate( [ { $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } }, { $limit : 5 } ] )
{
"result" : [
{
"_id" : {
"color" : "maroon",
"transport" : "motorbike"
},
"count" : 18
},
{
"_id" : {
"color" : "orange",
"transport" : "autombile"
},
"count" : 18
},
{
"_id" : {
"color" : "green",
"transport" : "train"
},
"count" : 18
},
{
"_id" : {
"color" : "purple",
"transport" : "train"
},
"count" : 18
},
{
"_id" : {
"color" : "grey",
"transport" : "plane"
},
"count" : 18
}
],
"ok" : 1
}
您现在可以看到添加到_id的传输元素中的额外字段,我们已经将结果限制为只有五个。现在,您应该看到我们如何从多个操作符构建管道,以从我们的集合中提取数据聚合信息。
$匹配
我们要查看的下一个操作符是$match,它用于有效地返回聚合管道中普通 MongoDB 查询的结果。$match操作符最好用在管道的开始,以限制最初放入管道的文档数量;通过限制处理的文档数量,我们显著降低了性能开销。例如,假设我们只想对那些num值大于 500 的文档执行管道操作。我们可以使用查询{ num : { $gt : 500 } }返回所有符合这个标准的文档。如果我们将这个查询作为一个$match添加到我们现有的聚合中,我们会得到以下结果:
[
{ $match : { num : { $gt : 500 } } },
{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } },
{ $limit : 5 }
]
这将返回以下结果:
{
"result" : [
{
"_id" : {
"color" : "white",
"transport" : "boat"
},
"count" : 9
},
{
"_id" : {
"color" : "black",
"transport" : "motorbike"
},
"count" : 9
},
{
"_id" : {
"color" : "maroon",
"transport" : "train"
},
"count" : 9
},
{
"_id" : {
"color" : "blue",
"transport" : "autombile"
},
"count" : 9
},
{
"_id" : {
"color" : "green",
"transport" : "autombile"
},
"count" : 9
}
],
"ok" : 1
}
您会注意到,返回的结果几乎与前面的示例完全不同。这是因为文档的创建顺序已经改变。因此,当我们运行这个查询时,我们限制了输出,删除了之前输出的原始文档。您还会看到,我们的计数是先前结果的一半。这是因为我们已经将潜在的数据集缩减到原来的一半。如果我们希望返回的结果保持一致,我们需要调用另一个管道操作符$sort。
$排序
正如您刚才看到的,$limit命令可以改变结果中返回的文档,因为它反映了执行聚合时文档最初输出的顺序。随着$sort命令的出现,这个问题可以得到解决。我们只需要在提供限制之前对特定字段进行排序,以便返回相同的有限结果集。$sort的语法与普通查询相同;您可以指定要排序的文档,正数表示升序,负数表示降序。为了展示这是如何工作的,让我们在有和没有匹配以及限制为 1 的情况下运行我们的查询。您将会看到,在$limit之前使用$sort,我们可以以相同的顺序返回文档。
这给了我们对
[
{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } },
{ $sort : { _id :1 } },
{ $limit : 5 }
]
该查询的结果是:
{
"result" : [
{
"_id" : {
"color" : "black",
"transport" : "autombile"
},
"count" : 18
}
],
"ok" : 1
}
第二个查询如下所示:
[
{ $match : { num : { $gt : 500 } } },
{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } },
{ $sort : { _id :1 } },
{ $limit : 1 }
]
该查询的结果是
{
"result" : [
{
"_id" : {
"color" : "black",
"transport" : "autombile"
},
"count" : 9
}
],
"ok" : 1
}
您会注意到两个查询现在包含相同的文档,它们只是在计数上有所不同。这意味着我们的排序已经在限制之前被应用,并允许我们得到一致的结果。这些操作符应该让您了解到,通过构建一个操作符管道来操纵事物,直到我们得到想要的结果,您可以驱动多大的力量。
$展开
我们要看的下一个操作符是$unwind。这需要一个数组,并为每个数组元素将每个元素拆分到一个新文档中(在内存中,而不是添加到您的集合中)。与创建 shell 管道一样,理解$unwind操作符输出内容的最佳方式就是自己运行它并评估输出。让我们来看看$unwind的结果:
db.aggregation.aggregate({ $unwind : "$vegetables" });
{
"result" : [
{
"_id" : ObjectId("51de841747f3a410e3000001"),
"num" : 1,
"color" : "blue",
"transport" : "train",
"fruits" : [
"orange",
"banana",
"kiwi"
],
"vegetables" : "corn"
},
{
"_id" : ObjectId("51de841747f3a410e3000001"),
"num" : 1,
"color" : "blue",
"transport" : "train",
"fruits" : [
"orange",
"banana",
"kiwi"
],
"vegetables" : "brocoli"
},
{
"_id" : ObjectId("51de841747f3a410e3000001"),
"num" : 1,
"color" : "blue",
"transport" : "train",
"fruits" : [
"orange",
"banana",
"kiwi"
],
"vegetables" : "potato"
},
...
],
"ok" : 1
}
现在,我们的结果数组中有 3000 个文档,每个文档都有自己的蔬菜和原始源文档的其余部分!你可以看到我们可以用$unwind做的事情的威力,以及如何用一个非常大的巨型文档集合给自己找麻烦。请始终记住,如果您首先运行匹配,那么在运行其他更密集的聚合操作之前,您可以减少要处理的对象的数量。
$项目
下一个操作符$project用于限制字段或重命名作为文档一部分返回的字段。这就像可以在find命令上设置的字段限制参数一样。这是减少聚合返回的多余字段的最佳方式。假设我们只想查看每个文档中的水果和蔬菜;我们可以提供一个文档,显示我们希望显示(或不显示)哪些元素,就像我们添加到我们的find命令中一样。举以下例子:
[
{ $unwind : "$vegetables" },
{ $project : { _id: 0, fruits:1, vegetables:1 } }
]
该投影返回以下结果:
{
"result" : [
{
"fruits" : [
"orange",
"banana",
"kiwi"
],
"vegetables" : "corn"
},
{
"fruits" : [
"orange",
"banana",
"kiwi"
],
"vegetables" : "brocoli"
},
{
"fruits" : [
"orange",
"banana",
"kiwi"
],
"vegetables" : "potato"
},
...
],
"ok" : 1
}
这比以前好,因为现在我们的文档没有以前那么大了。但是更好的办法是减少归还文件的数量。我们的下一个操作员会帮你的。
$跳过
$skip是与$limit操作符互补的管道操作符,但是它不是将结果限制在前 X 个文档,而是跳过前 X 个文档并返回所有其他剩余的文档。我们可以用它来减少归还文件的数量。如果我们用值 2995 将它添加到前面的查询中,我们将只返回五个结果。这将为我们提供以下查询:
[
{ $unwind : "$vegetables" },
{ $project : { _id: 0, fruits:1, vegetables:1 } },
{ $skip : 2995 }
]
结果是
{
"result" : [
{
"fruits" : [
"kiwi",
"pear",
"lemon"
],
"vegetables" : "pumpkin"
},
{
"fruits" : [
"kiwi",
"pear",
"lemon"
],
"vegetables" : "mushroom"
},
{
"fruits" : [
"pear",
"lemon",
"cherry"
],
"vegetables" : "pumpkin"
},
{
"fruits" : [
"pear",
"lemon",
"cherry"
],
"vegetables" : "mushroom"
},
{
"fruits" : [
"pear",
"lemon",
"cherry"
],
"vegetables" : "capsicum"
}
],
"ok" : 1
}
这就是如何使用$skip操作符来减少返回的条目数。您还可以使用互补的$limit操作符以同样的方式限制结果的数量,甚至可以将它们组合起来,在一个集合的中间挑选出一定数量的结果。假设我们想要 3000 个条目的数据集的结果 1500–1510。我们可以提供 1500 的$skip值和 10 的$limit,这将只返回我们想要的 10 个结果。
我们已经讨论了 MongoDB 聚合框架中的一些顶级管道操作符。有许多较小的运算符可以在顶级管道运算符中用作管道表达式。其中包括一些地理函数、数学函数(如平均值、第一名和最后一名)以及一些日期/时间和其他操作。所有这些都可以用来组合执行聚合操作,就像我们已经讨论过的那样。请记住,管道中的每个操作都将根据上一个操作的结果来执行,您可以输出并单步执行它们来创建您想要的结果。
数据处理
MapReduce 是 MongoDB 中最复杂的查询机制之一。它通过两个 JavaScript 函数来工作,map和reduce。这两个函数是完全由用户定义的,这给了您难以置信的灵活性!几个简短的例子将展示一些你可以用 MapReduce 做的事情。
MapReduce 的工作原理
在我们深入例子之前,最好先了解一下什么是 Map/Reduce 以及它是如何工作的。在 MongoDB 的 MapReduce 实现中,我们向给定的集合发出一个专门的查询,然后来自该查询的所有匹配文档都被输入到我们的map函数中。这个map函数被设计用来生成键/值对。然后,任何具有多个值的键集合都被输入到reduce函数,该函数返回输入数据的聚合结果。在这之后,还有一个可选的步骤,可以通过一个finalize函数完成数据的完美呈现。
设置测试文档
首先,我们需要设置一些文档来进行测试。我们已经创建了一个mapreduce集合,它是您之前恢复的test数据库的一部分。如果您还没有恢复它,请使用以下命令提取归档文件:
$ tar -xvf test.tgz
x test/
x test/aggregation.bson
x test/aggregation.metadata.json
x test/mapreduce.bson
x test/mapreduce.metadata.json
然后运行mongorestore命令来恢复test数据库:
$ mongorestore test
connected to: 127.0.0.1
Sun Jul 21 19:26:21.342 test/aggregation.bson
Sun Jul 21 19:26:21.342 going into namespace [test.aggregation]
1000 objects found
Sun Jul 21 19:26:21.350 Creating index: { key: { _id: 1 }, ns: "test.aggregation", name: "_id_" }
Sun Jul 21 19:26:21.688 test/mapreduce.bson
Sun Jul 21 19:26:21.689 going into namespace [test.mapreduce]
1000 objects found
Sun Jul 21 19:26:21.695 Creating index: { key: { _id: 1 }, ns: "test.mapreduce", name: "_id_" }
这将为您提供一个使用 MapReduce 的文档集合。首先,让我们看看世界上最简单的地图功能。
使用地图函数
该函数将从mapreduce集合中的每个文档“发出”颜色和num值。这两个字段将以键/值的形式输出,第一个参数(颜色)作为键,第二个参数(数字)作为值。这在开始时很难理解,所以让我们看看执行这个 emit 的简单的map函数:
var map = function() {
emit(this.color, this.num);
};
为了运行 Map/Reduce,我们还需要一个reduce函数,但是在做任何有趣的事情之前,让我们看看空的reduce函数的结果是什么,以了解会发生什么。
var reduce = function(color, numbers) { };
在您的 shell 中输入这两个命令,您就拥有了运行 MapReduce 所需的一切。
您需要提供的最后一件事是 MapReduce 要使用的输出字符串。这个字符串定义了这个 MapReduce 命令的输出应该放在哪里。两个最常见的选项是
- 收藏
- 到控制台(内嵌)
对于我们目前的目的,让我们输出到屏幕上,这样我们就可以看到到底发生了什么。为此,我们传递一个带有值为{ inline : 1 }的out选项的文档,如下所示:
{ out : { inline : 1 } }
这为我们提供了以下命令:
db.mapreduce.mapReduce(map,reduce,{ out: { inline : 1 } });
结果看起来像这样:
{
"results" : [
{
"_id" : "black",
"value" : null
},
{
"_id" : "blue",
"value" : null
},
{
"_id" : "brown",
"value" : null
},
{
"_id" : "green",
"value" : null
},
{
"_id" : "grey",
"value" : null
},
{
"_id" : "maroon",
"value" : null
},
{
"_id" : "orange",
"value" : null
},
{
"_id" : "purple",
"value" : null
},
{
"_id" : "red",
"value" : null
},
{
"_id" : "white",
"value" : null
},
{
"_id" : "yellow",
"value" : null
}
],
"timeMillis" : 95,
"counts" : {
"input" : 1000,
"emit" : 1000,
"reduce" : 55,
"output" : 11
},
"ok" : 1,
}
这表明每个“关键”颜色值是单独分离出来的,并且是每个文档的唯一_id值。因为我们没有为每个文档的值部分指定任何内容,所以它被设置为null。我们可以通过为我们想要的 MapReduce 结果添加输出部分来对此进行修改。在这种情况下,我们需要每个函数的概要。要做到这一点,我们可以使用函数来修改我们想要返回的对象,以代替null。在这种情况下,让我们返回每种颜色的所有值的总和。为此,我们可以创建一个函数,该函数将返回传递给reduce函数的每种颜色的所有数字数组的总和。幸运的是,我们可以使用一个叫做Array.sum的便利函数来对一个数组的所有值求和。这为我们提供了以下reduce功能:
var reduce = function(color, numbers) {
return Array.sum(numbers);
};
太好了。除了我们的内联输出,我们还可以让 MapReduce 写入一个集合;为此,我们只需用我们希望输出到的集合的名称替换那个{ inline : 1 }。所以让我们输出到一个叫做mrresult的集合。这为我们提供了以下命令:
db.mapreduce.mapReduce(map,reduce,{ out: "mrresult" });
当用我们新的reduce函数执行时,它给出了以下结果:
{
"result" : "mrresult",
"timeMillis" : 111,
"counts" : {
"input" : 1000,
"emit" : 1000,
"reduce" : 55,
"output" : 11
},
"ok" : 1,
}
如果您现在想要查看文档结果,您需要从mrresult集合中查询它们,如下所示:
> db.mrresult.findOne();
{ "_id" : "black", "value" : 45318 }
现在我们有了一个基本的工作系统,我们可以得到更高级的!
高级 MapReduce
假设我们想要的不是所有值的总和,而是平均值!这变得更加困难,因为我们需要添加另一个变量——我们拥有的对象数量!但是我们如何从map函数中传递两个变量呢?毕竟,emit 只接受两个参数。我们可以进行各种各样的“欺骗”;我们返回一个 JSON 文档,它可以有任意多的字段!因此,让我们扩展我们原来的 map 函数,返回一个包含颜色值和计数器值的文档。首先,我们将文档定义为一个新变量,填充 JSON 文档,然后发出该文档。
var map = function() {
var value = {
num : this.num,
count : 1
};
emit(this.color, value);
};
请注意,我们将计数器值设置为 1,以便对每个文档只计数一次!现在来看一下reduce函数。它需要处理我们之前创建的一系列有价值的文档。最后要注意的是,我们需要在我们的reduce函数的return函数中返回相同的值,这些值是在我们的map函数中创建并发送给我们的 emit 的。
Note
你也可以通过使用包含所有数字的数组的长度来完成我们在这里做的所有事情。但是通过这种方式,您可以看到更多关于 MapReduce 的功能。
为了处理这个数组,我们创建了一个简单的for循环,数组的长度,我们迭代每个成员,并将每个文档的num和count添加到新的返回变量reduceValue中。现在我们简单地返回这个值,我们有我们的结果。
var reduce = function(color, val ) {
reduceValue = { num : 0, count : 0};
for (var i = 0; i < val.length; i++) {
reduceValue.num += val[i].num;
reduceValue.count += val[i].count;
}
return reduceValue;
};
此时,你应该想知道这是如何得到我们的平均值的。我们有计数和数量,但没有实际的平均值!如果你再次运行 MapReduce,你可以看到自己的结果。现在,请注意,每次输出到一个集合时,MapReduce 都会在写入之前删除该集合!对我们来说,这是一件好事,因为我们只希望这次运行的结果,但它可能会在未来回来困扰你。如果您想合并两者的结果,您可以制作一个类似于{ out : { merge : "mrresult" } }的输出文档。
db.mapreduce.mapReduce(map,reduce,{ out: "mrresult" });
现在让我们快速检查一下这些结果:
> db.mrresult.findOne();
{
"_id" : "black",
"value" : {
"num" : 18381,
"count" : 27028,
}
}
不,没有平均值。这意味着我们有更多的工作要做,但是如果我们必须返回一个匹配 emit 输入的文档,我们如何计算平均值呢?我们需要第三个函数!MapRreduce 提供了一个名为finalize的函数。这允许您在返回 MapReduce 结果之前进行最后的清理。让我们编写一个函数,它将从reduce获取结果,并为我们计算平均值:
var finalize = function (key, value) {
value.avg = value.num/value.count;
return value;
};
是的,就这么简单。所以现在我们的map、reduce和finalize函数都准备好了,我们只需将它们添加到我们的调用中。在最后一个文档中设置了finalize选项;连同out,这给了我们以下命令:
db.mapreduce.mapReduce(map,reduce,{ out: "mrresult", finalize : finalize });
让我们从这里查询一个示例文档:
> db.mrresult.findOne();
{
"_id" : "black",
"value" : {
"num" : 45318,
"count" : 91,
"avg" : 498
}
}
现在好多了!我们有我们的数字,我们的计数,我们的平均值!
调试 MapReduce
调试 Map/Reduce 是一项相当耗时的任务,但是有几个小技巧可以让你的生活变得更轻松。首先让我们来看看调试一个map。您可以通过用函数重载 emit 来调试map,如下所示:
var emit = function(key, value) {
print("emit results - key: " + key + " value: " + tojson(value));
}
这个emit函数将返回与map函数相同的键和值结果。您可以使用map.apply()和您收集的示例文档来测试一个,如下所示:
> map.apply(db.mapreduce.findOne());
emit results - key: blue value: { "num" : 1, "count" : 1 }
既然您已经知道了对您的map的期望,那么您可以开始调试您的reduce了。你首先需要确认你的map和reduce以相同的格式返回——这很关键。接下来你可以做的是创建一个短数组,里面有一些值,就像传入你的reduce的那些值一样,如下所示:
a = [{ "num" : 1, "count" : 1 },{ "num" : 2, "count" : 1 },{ "num" : 3, "count" : 1 }]
现在可以如下调用reduce。这将允许您查看 emit 返回的值:
>reduce("blue",a);
{ "num" : 6, "count" : 3 }
如果所有其他方法都失败了,并且您对函数内部发生的事情感到困惑,不要忘记您可以使用printjson()函数将任何 JSON 值打印到mongodb日志文件中以供读取。在调试软件时,这总是一个有价值的工具。
摘要
到目前为止,您应该对 MongoDB 中的功能和灵活性有了确切的了解,使用了三个最强大、最灵活的查询系统。通过阅读本章,你应该知道如何使用文本索引在多种语言中执行强大的文本搜索。您应该能够使用 MongoDB 聚合框架创建高度复杂和灵活的聚合。最后,您现在应该能够使用强大的 JavaScript 支持的 MapReduce,这将允许您对数据编写强大的分组和转换。
九、数据库管理
Abstract
在本章中,我们将带您了解一些可以在 MongoDB 服务器上执行的基本管理操作。我们还将展示如何自动化其中的一些活动,比如备份您的服务器。
在本章中,我们将带您了解一些可以在 MongoDB 服务器上执行的基本管理操作。我们还将展示如何自动化其中的一些活动,比如备份您的服务器。
因为 MongoDB 是一个非关系数据库系统,所以不需要数据库管理员执行的许多更传统的功能。例如,没有必要在服务器上创建新的数据库、集合或字段,因为 MongoDB 会在您访问它们时动态创建这些元素。因此,在绝大多数情况下,您不需要管理数据库和模式。
然而,这种不必预定义一切的自由可能会导致无意中创建元素,比如文档中无关的集合和字段。管理员和开发者偶尔需要从数据库中清除未使用的数据元素,尤其是在项目的开发阶段,此时变化通常很快。在最终确定解决方案和清理数据库之前,他们可能必须尝试许多方法。MongoDB 的易用性鼓励了这种探索性的开发模式;但是,这也可能导致数据存储中的混乱,因为创建数据结构所需的工作量几乎为零。
造成这种混乱的一个因素,也是 MongoDB 和 SQL 数据库之间的一个更重要的区别,是 MongoDB 中的所有对象和元素名称在所有平台上都是区分大小写的。因此,foo和Foo集合名称指的是两个完全不同的集合。因此,您需要小心使用数据库和集合命名,以避免意外创建多个仅在名称大小写上不同的数据库。(然而,从 MongoDB 2.4 开始,有一个例外:您不能再创建名称只有大小写不同的数据库,因为这样做会产生错误。)
这些数据库的不同版本将会填满您的磁盘,并且由于允许开发者和系统的最终用户连接到不完整的或非预期的数据集,它们可能会给开发者和系统的最终用户带来许多困惑。
在本章中,您将学习如何执行以下所有任务:
- 备份并恢复您的 MongoDB 系统。
- 使用提供的
MongoDBshell(通过mongo命令调用)执行常见任务。 - 通过身份验证控制对服务器的访问。
- 监控您的数据库实例。
然而,在深入研究这些任务之前,我们先来看看用来执行其中许多任务的工具。
使用管理工具
管理员需要适合于执行保持服务器平稳运行的日常任务的工具。MongoDB 包中有一些非常好的工具,以及一个不断发展的有用的第三方工具集合。以下部分涵盖了一些最重要的可用工具,以及如何使用它们。
蒙戈,蒙戈布控制台
作为管理员,您将使用的主要工具是mongo,MongoDB 控制台工具。mongo是一个基于 JavaScript 的命令行控制台实用程序。它类似于主流关系数据库提供的许多查询工具。然而,mongo有一个独特的锦囊妙计:它可以运行用 JavaScript 编写的程序,直接与 MongoDB 数据库交互。
这个控制台允许您用 JavaScript 编写与 MongoDB 的所有交互,然后将这些脚本存储在.js文件中,在需要时运行。事实上,mongo控制台中的许多内置命令本身就是用 JavaScript 编写的。
您可以将任何要在命令 shell 中输入的命令放入一个扩展名为.js的文件中,并在启动 shell 或在 shell 中使用load()函数时,通过简单地将文件名添加到命令行中来运行它们。shell 将执行文件的内容,然后退出。这对于运行重复命令列表很有用。
在本章中,我们将使用mongo控制台来演示您可以在 MongoDB 服务器上执行的许多管理任务,因为它是随 MongoDB 服务器一起分发的,所以我们可以保证它会在那里。
使用第三方管理工具
MongoDB 提供了几个第三方管理工具。MongoDB,Inc .在 MongoDB 网站上维护了一个页面,其中列出了当前可用的第三方工具。你可以在 http://docs.mongodb.org/ecosystem/tools/administration-interfaces/ 找到这个名单。
这些工具中有许多是基于 web 的,原则上类似于 phpMyAdmin for MySQL,但有些也是成熟的桌面 ui。
备份 MongoDB 服务器
新的 MongoDB 管理员应该学习的第一个技能是如何备份和恢复 MongoDB 服务器。用这些知识武装自己会让你在探索一些更高级的管理功能时感觉更舒服,因为你知道你的宝贵数据被安全地存储在某个地方。
创建备份 101
让我们从执行一个简单的备份开始,然后恢复它。在这个过程中,您将确保备份完好无损,并且您将看到一些说明备份和恢复功能如何工作的实际例子。一旦您对如何使用这些特性有了坚实的理解,您将能够继续探索 MongoDB 更高级的管理特性。
在这个简单的备份示例中,我们假设如下:
- 您的 MongoDB 服务器运行在您当前登录的同一台机器上。
- 您有足够的磁盘空间来存放转储文件,这些文件的大小最多与您的数据库相同。
- 您的备份将在您的主目录中进行。这意味着您不必处理任何与权限相关的问题。
MongoDB 备份实用程序名为mongodump;该实用程序作为标准发行版的一部分提供。以下示例将正在运行的 MongoDB 服务器简单备份到指定的磁盘目录:
$> cd ∼
$> mkdir testmongobackup
$> cd testmongobackup
$> mongodump
当mongodump运行时,您应该看到它输出如下所示的内容:
$ mongodump
connected to: 127.0.0.1
Tue May 21 20:52:58.639 all dbs
Tue May 21 20:52:58.640 DATABASE: blog to dump/blog
Tue May 21 20:52:58.640 blog.system.indexes to dump/blog/system.indexes.bson
Tue May 21 20:52:58.641 4 objects
Tue May 21 20:52:58.641 blog.system.profile to dump/blog/system.profile.bson
Tue May 21 20:52:58.645 3688 objects
Tue May 21 20:52:58.645 Metadata for blog.system.profile to dump/blog/system.profile.metadata.json
Tue May 21 20:52:58.645 blog.authors to dump/blog/authors.bson
Tue May 21 20:52:58.646 1 objects
Tue May 21 20:52:58.646 Metadata for blog.authors to dump/blog/authors.metadata.json
Tue May 21 20:52:58.646 blog.posts to dump/blog/posts.bson
Tue May 21 20:52:58.686 29997 objects
Tue May 21 20:52:58.709 Metadata for blog.posts to dump/blog/posts.metadata.json
Tue May 21 20:52:58.710 blog.tagcloud to dump/blog/tagcloud.bson
Tue May 21 20:52:58.710 1 objects
Tue May 21 20:52:58.710 Metadata for blog.tagcloud to dump/blog/tagcloud.metadata.json
如果您的输出看起来与此不太相似,那么您应该仔细检查您的环境是否与前面陈述的假设相匹配。
如果您确实看到了正确的输出,那么您的数据库已经备份到了testmongobackup/dump目录。以下代码片段将数据库还原到执行备份时的状态:
$> cd ∼/testmongobackup
$> mongorestore --drop
connected to: 127.0.0.1
Tue May 21 20:53:46.337 dump/blog/authors.bson
Tue May 21 20:53:46.337 going into namespace [blog.authors]
Tue May 21 20:53:46.337 dropping
1 objects found
Tue May 21 20:53:46.338 Creating index: { key: { _id: 1 }, ns: "blog.authors", name: "_id_" }
Tue May 21 20:53:46.339 dump/blog/posts.bson
Tue May 21 20:53:46.339 going into namespace [blog.posts]
Tue May 21 20:53:46.339 dropping
29997 objects found
Tue May 21 20:53:47.284 Creating index: { key: { _id: 1 }, ns: "blog.posts", name: "_id_" }
Tue May 21 20:53:47.375 Creating index: { key: { Tags: 1 }, ns: "blog.posts", name: "Tags_1" }
Tue May 21 20:53:47.804 dump/blog/system.profile.bson
Tue May 21 20:53:47.804 skipping
Tue May 21 20:53:47.804 dump/blog/tagcloud.bson
Tue May 21 20:53:47.804 going into namespace [blog.tagcloud]
Tue May 21 20:53:47.804 dropping
1 objects found
Tue May 21 20:53:47.821 Creating index: { key: { _id: 1 }, ns: "blog.tagcloud", name: "_id_" }
--drop选项告诉mongorestore实用程序在恢复之前丢弃数据库中的每个集合。因此,备份的数据会替换数据库中当前的数据。如果您选择不使用--drop选项,恢复的数据将被附加到每个集合的末尾,这将导致重复的项目。
让我们更仔细地检查一下这个例子中发生了什么。
默认情况下,mongodump实用程序使用默认端口连接到本地数据库,提取与每个数据库和集合相关的所有数据,并将它们存储在预定义的文件夹结构中。
由mongodump创建的默认文件夹结构采用以下形式:
./dump/[databasename]/[collectionname].bson
示例中使用的数据库系统由一个名为blog的数据库组成。blog数据库包含三个集合:authors、posts和tagcloud。
mongodump将从数据库服务器获取的数据保存在.bson文件中,这些文件是 MongoDB 内部用来存储文档的内部 BSON 格式的副本。在前面的示例中,您还可以看到正在还原的每个集合的索引。MongoDB 服务器维护索引,它记录每个集合的索引定义,这些定义存储在metadata.json文件中。正是这些元数据文件允许您在从备份恢复时重建索引。
转储数据库后,您可以将文件夹存档并存储在任何在线或离线媒体上,如 CD、USB 驱动器、磁带或 S3 格式。
Note
在将备份文件写入目录之前,mongodump实用程序不会清空output目录的内容。如果您在此目录中有现有内容,它们不会被删除,除非它们与mongodump被指示备份的文件(collectionname .bson)的名称相匹配。如果您希望将多个集合转储添加到同一个转储目录,这很好;但是,如果每次备份数据时都使用同一个转储目录,但不清除它,这可能会导致问题。例如,假设您有一个定期备份的数据库,并且在某个时候您决定从该数据库中删除一个集合。除非您清除正在执行备份的目录,或者手动删除与已删除的集合相关联的文件,否则下次还原数据时,已删除的集合将会重新出现。除非您想在备份中覆盖数据,否则您应该确保在使用mongodump之前清空目标目录。
备份单个数据库
当您在同一台服务器上运行多个应用时,您通常会发现自己想要单独备份每个数据库,而不是像前面的示例那样一次备份所有数据库。
使用mongodump,您可以通过在命令行中添加-d database_name选项来实现这一点。这导致mongodump创建./dump文件夹;但是,该文件夹将只包含单个数据库的备份文件。
备份单个集合
假设您有一个博客站点,其中的authors集合的内容变化不大。相反,博客站点快速变化的内容包含在posts和tagcloud集合中。您可能一天只备份一次整个数据库,但希望每小时备份一次这两个集合。幸运的是,使用 mongodump,您可以通过使用-c选项来指定您希望备份的集合,从而轻松地做到这一点。
mongodump实用程序不会清除其目标目录。这意味着,对于您想要备份的每个集合,您可以连续调用mongodump来将一个给定的集合添加到您的备份中,如下例所示:
$mkdir ∼/backuptemp
$cd ∼/backuptemp
$mongodump -d blog -c posts
$mongodump -d blog -c tagcloud
...
archive the dump folder ∼/backuptemp away as a tar file
...
$ cd ∼
$ rm -rf backuptemp
深入了解备份
至此,您已经知道如何执行备份和随后恢复数据的基本任务。现在,您已经准备好查看一些强大的选项,这些选项允许您定制 MongoDB 的备份和恢复功能,以满足您的特定需求。
mongodump实用程序包括图 9-1 中所示的选项,通过在 MongoDB 2.5.3 中运行help来捕获。

图 9-1。
The mongodump utility help display showing its options
此处列出的大多数选项都是不言自明的,但以下选项除外:
--dbpatharg:如果您有大量的数据需要备份,并且您不关心索引的备份,那么最好通过将 MongoDB 服务器使用的数据文件直接复制到备份介质来备份数据库。该选项允许您直接从服务器的数据文件进行备份,但它只能在服务器脱机或被写保护的情况下使用(有关详细信息,请参阅本章后面的“备份大型数据库”一节)。--directoryperdb:将这个命令行选项与--dbpath选项结合使用,指定正在备份的 MongoDB 服务器被配置为将其每个数据库的数据文件放在一个单独的目录中。默认情况下,MongoDB 将其所有数据文件放在一个目录中。仅当您已将服务器配置为在此模式下运行时,才应使用此选项。-
o [ --out ]arg:使用该选项可以指定数据库转储的存放目录。默认情况下,mongodump实用程序在当前目录下创建一个名为/dump的文件夹,并将转储文件写入其中。您可以使用-o/--out选项来选择放置输出转储的替代路径。
--authenticationDatabasearg:指定保存用户凭证的数据库。Mongodump将默认使用没有该选项的–db指定的数据库。--authenticationMechanismarg:默认为 MongoDB 的挑战/响应(用户名/密码)机制。这个命令用于切换到 MongoDB Enterprise edition 的 Kerberos 身份验证。
还原单个数据库或集合
您已经看到了mongodump实用程序如何备份单个数据库或集合;mongorestore实用程序具有同样的灵活性。如果从中恢复的转储目录中有所需集合或数据库的备份文件,您可以使用mongorestore来恢复项目;您不需要恢复备份中存在的所有项目。如果您愿意,可以单独还原它们。
让我们从查看mongorestore中可用的选项开始,如图 9-2 所示。

图 9-2。
The mongorestore help display showing its options
你可能从对mongodump的讨论中认识到这些选项中的大部分;但是,以下两个选项值得特别一提:
--drop:该选项指示mongorestore在恢复之前删除现有集合。这有助于确保没有重复。如果不使用此选项,还原的数据将被追加(插入)到目标集合中。--noobjcheck:该选项指示mongorestore在将对象插入到destination集合之前忽略验证对象的步骤。
还原单个数据库
您可以使用mongorestore实用程序的-d选项来恢复单个数据库。和以前一样,如果数据库已经存在于您的 MongoDB 服务器中,不要忘记使用--drop选项:
$cd ∼/testmongobackup
$mongorestore -d blog --drop
还原单个集合
使用类似的语法将单个集合还原到数据库;不同之处在于,您还可以使用-c选项指定集合名称,如下例所示:
$cd ∼/testmongobackup
$mongorestore -d blog -c posts --drop
自动化备份
对于小型安装或开发者设置,运行mongodump实用程序并保存结果的简单操作是执行临时备份的一种非常合适的方法。例如,Mac OS X 工作站上的常见做法是让 Time Machine(Mac 备份实用程序)存储备份。
对于任何类型的生产设置,您都希望自动备份服务器;如果您遇到任何问题,定期备份可以帮助您避免麻烦或从麻烦中恢复。这不仅适用于您的安装(例如,如果您有损坏的数据库),而且适用于您的用户无意中损坏或破坏数据的情况。
让我们来看一些简单的脚本,您可以使用它们来自动化您的备份。
使用本地数据存储
如果您的系统连接了大型备份驱动器,或者您可以通过 NFS 或 SMB 挂载外部文件系统,那么一个在指定目录中创建归档文件的简单备份脚本就足够了。以下备份脚本易于设置;只需编辑脚本顶部的变量,以匹配本地系统的变量:
#!/bin/bash
##########################################
# Edit these to define source and destinations
MONGO_DBS=""
BACKUP_TMP=∼/tmp
BACKUP_DEST=∼/backups
MONGODUMP_BIN=/usr/bin/mongodump
TAR_BIN=/usr/bin/tar
##########################################
BACKUPFILE_DATE=date +%Y%m%d-%H%M``
# _do_store_archive <Database> <Dump_dir> <Dest_Dir> <Dest_file>
function _do_store_archive {
mkdir -p $3
cd $2
tar -cvzf $3/$4 dump
}
# _do_backup <Database name>
function _do_backup {
UNIQ_DIR="$BACKUP_TMP/$1"date "+%s"``
mkdir -p $UNIQ_DIR/dump
echo "dumping Mongo Database $1"
if [ "all" = "$1" ]; then
$MONGODUMP_BIN -o $UNIQ_DIR/dump
else
$MONGODUMP_BIN -d $1 -o $UNIQ_DIR/dump
fi
KEY="database-$BACKUPFILE_DATE.tgz"
echo "Archiving Mongo database to $BACKUP_DEST/$1/$KEY"
DEST_DIR=$BACKUP_DEST/$1
_do_store_archive $1 $UNIQ_DIR $DEST_DIR $KEY
rm -rf $UNIQ_DIR
}
# check to see if individual databases have been specified, otherwise backup the whole server
# to "all"
if [ "" = "$MONGO_DBS" ]; then
MONGO_DB="all"
_do_backup $MONGO_DB
else
for MONGO_DB in $MONGO_DBS; do
_do_backup $MONGO_DB
done
fi
表 9-1 列出了您必须更改的变量,以使这个简单的备份脚本适用于您的系统。
表 9-1。
The Variables Used in the Local Datastore Backup Script
| 可变的 | 描述 | | --- | --- | | `MONGO_DBS` | 将此变量留空("")可备份本地服务器上的所有数据库。或者您可以将数据库列表放入其中,以备份选定的数据库(`"db1 db2 db3"`)。 | | `BACKUP_TMP` | 将此变量设置为适合保存备份转储文件的临时目录。创建归档文件后,该目录中使用的临时数据将被删除。请务必选择一个与使用您的脚本相关的合适目录。例如,如果您使用脚本在本地帐户中创建备份,请使用`∼/tmp`;如果您将它用作在系统帐户下运行的系统 cronjob,请使用`/tmp`。在 Amazon EC2 实例上,您可能应该使用`/mnt/tmp`,这样文件夹就不会创建在系统根分区上,这个分区非常小。 | | `BACKUP_DEST` | 此变量保存备份的目标文件夹,并将在此文件夹下创建单独的文件夹。同样,将该目录放在与您使用备份脚本的方式相关的位置。 | | `MONGODUMP_BIN` | 因为您的备份脚本可能在没有设置完整路径集的帐户下运行,所以使用该变量指定该二进制文件的完整路径是明智的。您可以通过在终端窗口中键入`which mongodump`来确定系统上的适当路径。 | | `TAR_BIN` | 使用这个变量来设置 tar 二进制文件的完整路径;在终端窗口中使用`which tar`来确定该路径。 |您现在可以使用此脚本来备份您的数据库;这样做将在指定的BACKUP_DEST directory创建一组归档备份。创建的文件遵循以下命名格式:
Database Name``/``database-YYYYMMDD-HHMM
例如,以下代码片段显示了本章测试数据库的备份名称:
Backups:$ tree
.
|-- blog
| |-- database-20100611-1144.tgz
| -- database-20100611-1145.tgz`
``-- all`
|-- database-20100611-1210.tgz
|-- database-20100611-1221.tgz
|-- database-20100611-1222.tgz
|-- database-20100611-1224.tgz
``-- database-20100611-1233.tgz`
当然,你还需要安装脚本。如果您想每天运行这个脚本,只需将它放入/etc/cron.daily并重启cron服务以激活它。这种方法适用于大多数 Linux 发行版,比如 Ubuntu、Fedora、CentOS 和 RedHat。如果您想要不太频繁的备份,只需将脚本移动到/etc/cron.weekly或/etc/cron.monthly。对于更频繁的备份,您可以使用/etc/cron.hourly。
使用远程(基于云的)数据存储
上一节描述的脚本有一个创建和存储归档文件的独立函数。这使得修改脚本变得相对容易,以便它使用外部数据存储来存储备份归档。表 9-2 提供了几个例子,但是更多的其他机制也是可能的。
表 9-2。
Remote (Cloud-Based) Backup Storage Options
| 方法 | 描述 | | --- | --- | | rsync/ftp/tftp 或 scp 到另一台服务器 | 您可以使用 rsync 将归档文件移动到备份存储机器上。 | | s3 存储 | 如果你在 EC2 上运行你的系统,S3 存储是一个放置备份的好地方,因为存储成本很低,而且亚马逊会制作冗余副本。 |我们将检查存储备份的 S3 方法;然而,同样的原则适用于任何其他机制。
这个例子使用了 http://s3tools.org 中的s3cmd实用程序(用 python 编写)。在 Ubuntu 上,你可以使用sudo apt-get install s3cmd命令安装这个脚本;在 Mac OSX 上,这个脚本可以从MacPorts集合中获得。在 Fedora、CentOS、RedHat 上,可以从 http://s3tools.org 获取yum包,然后使用yum安装。
一旦你安装了这个包,运行s3cmd –configure来设置你的亚马逊 S3 凭证。注意,你只需要提供两个键:AWS_ACCESS_KEY和AWS_SECRET_ACCESS_KEY。s3cmd实用程序将创建一个配置文件,该文件包含您需要的信息:∼/.s3cfg。
以下是您需要对备份脚本进行的更改,以便与 S3 一起使用:
# _do_store_archive <Database> <Dump_dir> <Dest_Dir> <Dest_file>
BACKUP_S3_CONFIG=∼/.s3cfg
BACKUP_S3_BUCKET=somecompany.somebucket
S3CMD_BIN=/usr/bin/s3cmd
function _do_store_archive {
UNIQ_FILE="aws"date "+%s"``
cd $2
tar -cvzf $BACKUP_TMP/$UNIQ_FILE dump
$S3CMD_BIN --config $BACKUP_S3_CONFIG put $BACKUP_TMP/$UNIQ_FILE \
s3://$BACKUP_S3_BUCKET/$1/$4
rm -f $BACKUP_TMP/$UNIQ_FILE
}
表 9-3 列出了您需要配置的一些变量,以使这个改编的脚本工作。
表 9-3。
Configuring the Variables of Your Adapted Backup Script
| 可变的 | 描述 | | --- | --- | | `BACKUP_S3_CONFIG` | 运行`s3cmd –configure`以保存您的 S3 帐户详细信息时创建的`s3cmd`配置文件的路径。 | | `BACKUP_S3_BUCKET` | 希望脚本存储备份的存储桶的名称。 | | `S3CMD_BIN` | 到`s3cmd`可执行程序的路径,再次使用`which s3cmd`在您的系统上找到它。 |备份大型数据库
使用大型数据库系统时,创建有效的备份解决方案可能会成为一个问题。通常,制作数据库副本所花费的时间很长;甚至可能需要几个小时才能完成。在此期间,您必须将数据库保持在一致的状态,这样备份就不会包含在不同时间点复制的文件。数据库备份系统的圣杯是时间点快照,它可以很快完成。快照完成得越快,数据库服务器必须冻结的时间窗口就越小。
使用隐藏的辅助服务器进行备份
用于执行大型备份的一种技术是从隐藏的辅助节点进行备份,该辅助节点可以在备份时被冻结。备份完成后,该辅助服务器将重新启动以跟上应用。
MongoDB 使用 MongoDB 的复制机制,使得设置隐藏的辅助服务器并让它跟踪主服务器变得非常简单。这也相对容易配置(关于如何设置隐藏的二级设备的更多细节,参见第 11 章)。
使用日志文件系统创建快照
许多现代卷管理器能够创建驱动器在任何特定时间点的状态快照。使用文件系统快照是创建 MongoDB 实例备份的最快、最有效的方法之一。虽然设置这些系统超出了本书的范围,但是我们可以向您展示如何将 MongoDB 服务器置于这样一种状态,即它的所有数据在磁盘上都处于一致的状态。我们还向您展示了如何阻止写入,以便进一步的更改不会写入磁盘,而是缓冲在内存中。
快照允许您准确读取拍摄快照时的驱动器。系统的卷或文件系统管理器确保在拍摄快照后磁盘上发生更改的任何数据块不会被写回到驱动器上的同一位置;这将保留磁盘上所有要读取的数据。通常,使用快照的过程是这样的:
Create a snapshot. Copy data from the snapshot or restore the snapshot to another volume, depending on your volume manager. Release the snapshot; doing so releases all preserved disk blocks that are no longer needed back into the free space chain on the drive. Back up the data from the copied data while the server is still running.
刚才描述的方法的优点是,在拍摄快照时,对数据的读取可以不受阻碍地继续。
具有此功能的一些卷管理器包括:
- Linux 和 LVM 卷管理系统
- Sun ZFS
- 亚马逊 EBS 卷
- 使用卷影副本的 Windows Server
这些卷管理器中的大多数都能够在非常短的时间内(通常只有几秒钟)执行快照,即使数据量非常大也是如此。此时,卷管理器实际上并不复制数据;相反,它们实际上是在驱动器上插入一个书签,这样您就可以读取拍摄快照时驱动器的状态。
一旦备份系统从快照中读取了驱动器,那么随后被改变的旧块可以被释放回驱动器的自由空间链(或者文件系统用来标记自由空间的任何机制)。
为了使这成为创建备份的有效方法,我们必须让 MongoDB 日志文件存在于同一设备上,或者让 MongoDB 将所有未完成的磁盘写入刷新到磁盘,以便我们可以拍摄快照。强制 MongoDB 进行这种刷新的特性称为 fsync 阻止进一步写入的功能称为锁。MongoDB 能够同时执行这两种操作,因此在 fsync 之后,在释放锁之前,不会对磁盘进行进一步的写操作。通过将日志放在同一个设备上或执行 fsync 和 lock,我们可以使数据库在磁盘上的映像保持一致,并确保它在我们完成快照之前保持一致。
使用以下命令使 MongoDB 进入 fsync 和锁定状态:
$mongo
>use admin
>db.fsyncLock()
{
"info" : "now locked against writes",
"ok" : 1
}
您可以使用以下命令来检查锁的当前状态:
$mongo
>use admin
>db.currentOp()
{
"inprog" : [
],
"fsyncLock" : 1
}
"fsyncLock": 1状态表示 MongoDB 的 fsync 进程(负责将更改写入磁盘)当前被阻止执行写操作。
此时,您可以发出任何命令,让您的卷管理器创建 MongoDB 存储其数据文件的文件夹的快照。快照完成后,您可以使用以下命令来释放锁定:
$mongo
>db.fsyncUnlock();
{ "ok" : 1, "info" : "unlock requested" }
请注意,在释放锁之前可能会有一个小的延迟;但是,您可以使用db.currentOp()功能来检查结果。
当锁最终被清除时,db.currentOp()将返回以下内容:
$mongo
>use admin
>db.currentOp()
{ "inprog" : [] }
{ "inprog" : [] }行意味着锁已经被释放,MongoDB 可以再次开始写入磁盘。
现在您已经插入了快照书签,您可以使用与卷管理器相关的实用程序将快照的内容复制到合适的位置,以便存储备份。备份完成后,不要忘记释放快照。
有关快照的更多信息,请访问以下链接:
http://docs.mongodb.org/manual/tutorial/backup-databases-with-filesystem-snapshots/http://tldp.org/HOWTO/LVM-HOWTO/snapshots_backup.htmlhttp://docs.huihoo.com/opensolaris/solaris-zfs-administration-guide/html/ch06.htmlhttp://support.rightscale.com/09–Clouds/AWS/02-Amazon_EC2/EBS/Create_an_EBS_Snapshot
用于卷管理器的磁盘布局
一些卷管理器可以对一个分区上的子目录进行快照,但大多数不能,所以最好将计划用来存储 MongoDB 数据的卷挂载到文件系统上的一个合适的位置(例如,/mnt/mongodb),并使用服务器配置选项将数据目录、配置文件和任何其他与 MongoDB 相关的文件(例如,journal)单独放在这个挂载上。
这意味着当您拍摄卷的快照时,您捕获了服务器的完整状态,包括其配置。将服务器发行版的二进制文件直接放在该卷上可能是个好主意,这样您的备份就包含了一组完全协调的组件。
将数据导入 MongoDB
有时,您需要将大量数据加载到 MongoDB 中,作为参考数据使用。这些数据可能包括邮政编码表、ip 地理位置表、零件目录等。
MongoDB 包括一个批量“加载器”mongoimport,用于将数据直接导入服务器上的特定集合;这与mongorestore不同,后者设计用于从备份中恢复 MongoDB 二进制文件。
mongoimport实用程序可以从三种文件格式中的任何一种加载数据:
CSV: In this file format, each line represents a document, and fields are separated by commas. TSV: This file format is similar to CSV; however, it uses a tab character as the delimiter. This format is popular because it does not require the escaping of any text characters other than those for new lines. JSON: This format contains one block of JSON per line that represents a document. Unlike the other formats, JSON can support documents with variable schemas.
这个工具的使用相当直观。对于输入,它接受三种格式之一的文件、一个字符串或一个带有一组列标题名的文件(它们构成了 MongoDB 文档中的元素名),以及几个用于控制数据解释方式的选项。图 9-3 显示了如何使用mongoimport实用程序。

图 9-3。
The mongoimport help display showing its options
以下选项值得进一步解释:
--headerline:使用文件的第一行作为字段名列表。请注意,这仅适用于 CSV 和 TSV 格式。--ignoreblanks:不导入空字段。如果字段为空,则不会在文档中为该行创建相应的元素;如果不调用这个选项,那么就会创建一个列名为的空元素。--drop:删除一个集合,然后仅使用此次导入的数据重新创建它;否则,数据将被追加到集合中。
当使用mongoimport通过-d和-c选项导入数据时,还必须指定数据库名称和集合名称,如下例所示:
$mongoimport -d blog -c tagcloud --type csv --headerline < csvimportfile.csv
从 MongoDB 导出数据
mongoexport实用程序类似于mongoimport,但是mongoexport,顾名思义,从现有的 MongoDB 集合创建导出文件。这是从 MongoDB 实例中以其他数据库或电子表格应用可以读取的格式提取数据的最佳方式之一。图 9-4 显示了如何使用mongoexport实用程序。

图 9-4。
The mongoexport help display showing its options
mongoexport实用程序中值得注意的选项包括:
-q:指定用于定位要输出的记录的查询。这个查询可以是任何 JSON 查询字符串(但不是 JavaScript 查询字符串,因为它通常不像预期的那样工作),您可以使用它和db.collection.find()函数来选择记录的子集。如果您没有指定这个选项或者您将它设置为{},那么mongoexport实用程序将输出所有记录。-
f:列出要导出的数据库元素名称。
以下示例说明了如何使用mongoexport实用程序的选项:
$mongoexport -d blog -c posts -q {} -f _id,Title,Message,Author --csv >blogposts.csv
connected to: 127.0.0.1
exported 1 records
通过限制对 MongoDB 服务器的访问来保护数据
在某些情况下,您的应用可能会处理敏感数据,例如社交网络中的用户记录或电子商务应用中的支付细节。在许多情况下,有规则要求您必须确保对数据库系统中敏感数据的受限访问。
MongoDB 支持一个简单的基于角色的认证系统,该系统允许您控制谁有权访问每个数据库,以及他们被授予的访问级别。
在 MongoDB 服务器上,大多数更改数据配置或对其结构进行重大修改的命令都被限制为只能在每次新安装 MongoDB 时自动创建的特殊的admin数据库中运行。
在发出这些命令之前,您必须使用use admin命令切换到admin数据库。接下来的章节将会提到任何只允许管理员使用的命令,所以在你使用它之前,你总是知道你什么时候需要在admin数据库中。本章假设您可以选择数据库,并在必要时对其进行验证。
默认情况下,MongoDB 不使用任何身份验证方法。任何能够访问网络连接的人都可以连接到服务器并向其发出命令。但是,您可以向任何数据库添加用户,并且 MongoDB 可以配置为要求连接和控制台身份验证来访问相关的数据库。这是限制访问管理功能的推荐机制。
通过身份验证保护您的服务器
MongoDB 支持一个简单的身份验证模型,该模型允许管理员在每个用户的基础上限制对数据库的访问。
MongoDB 支持每个数据库上的单独访问控制记录;这些记录存储在一个特殊的system.users集合中。对于能够访问两个数据库(例如,db1和db2)的普通用户,他们的凭证和权限必须添加到这两个数据库中。
如果您为同一用户在不同的数据库上创建单独的登录和访问权限,则这些记录之间不会同步。换句话说,更改一个数据库上的用户密码不会更改任何其他数据库上的密码。然而,MongoDB 团队在 2.4 版本中引入了一种新的机制来允许委托凭证。以这种方式使用这些凭证,您可以创建一个带有密码的主用户。然后在其他数据库上创建用户,并指定该用户已经存在于 master 数据库中,并且应该使用其凭据进行身份验证。
这条规则还有一个最后的(也是关键的)例外:任何添加到特殊的admin数据库的用户将对所有数据库拥有相同的访问权限;您不需要为这些用户单独分配权限。
Note
如果您在添加 admin 用户之前启用了身份验证,那么您将只能通过 localhost 访问您的数据库,这意味着从托管 MongoDB 实例的机器建立连接。这是一项安全功能,旨在允许管理员在启用身份验证后创建用户。
添加管理员用户
添加admin用户就像切换到admin数据库并使用addUser()函数一样简单:
$mongo
> use admin
> db.addUser({user : "admin", pwd: "pass", roles: [ "readWrite", "dbAdmin" ] })
{
"user" : "admin",
"pwd" : "e4e538f5dcb52537cad02bbf8491693c",
"roles" : [
"readWrite",
"dbAdmin"
],
"_id" : ObjectId("5239915b1ce3dc1efebb3c84")
}
此时,您只需要添加一个admin用户;一旦定义了该用户,就可以使用它将其他的admin用户添加到admin数据库中,或者将普通用户添加到任何其他数据库中。
启用身份验证
现在,您需要更改服务器的配置来启用身份验证。为此,请停止您的服务器,并将--auth添加到启动参数中。
如果你用打包的安装程序安装了 MongoDB,比如yum或者 Aptitude,那么通常你可以编辑/etc/mongodb.conf来启用auth=true。接下来,您可以使用以下命令重新启动服务器并启用身份验证:
$sudo service mongodb restart
除了auth之外,您还可以使用 keyfile,这是一个包含某种描述的预共享密钥的文件,用于确认 MongoDB 节点之间的通信。要创建密钥文件,只需创建一个简单的文件,其中包含要使用的短语或字符串。然后像处理auth一样添加选项keyfile=/path/to/keyfile。你甚至可以删除旧的auth=true选项,因为用keyfile运行意味着auth。
在 mongo 控制台中验证
在运行admin数据库中的受限命令之前,您需要被认证为admin用户,如下例所示:
$mongo
> use admin
switched to db admin
>show collections
Sun May 26 17:22:26.132 JavaScript execution failed: error: {
"$err" : "not authorized for query on admin.system.namespaces",
"code" : 16550
} at src/mongo/shell/query.js:L131 }
>db.auth("admin", "pass");
1
此时,mongo控制台会打印出1(认证成功)或0(认证失败):
1
>show collections
system.indexes
system.users
如果您的身份验证成功,您将能够根据您的用户权限执行任何可用的操作。
如果您的身份验证不成功,那么您需要检查您的用户名/密码是否正确,以及admin用户是否已正确添加到admin数据库中。重置您的服务器,使其没有身份验证,然后使用以下命令列出admin数据库中system.users集合的内容:
$mongo
>use admin
> db.system.users.find()
{ "_id" : ObjectId("5239915b1ce3dc1efebb3c84"), "user" : "admin", "pwd" : "e4e538f5dcb52537cad02bbf8491693c", "roles" : [ "readWrite", "dbAdmin" ] }
Note
如果您使用一个admin凭证来访问除了admin之外的数据库,那么您必须首先通过admin数据库的认证。否则,您将无法访问系统中的任何其他数据库。
mongo控制台显示user集合的内容,使您能够看到什么是userid,而密码显示为您提供的原始密码的 MD5 散列:
$ mongo
> use blog
switched to db blog
> show collections
Wed Sep 18 21:42:51.855 JavaScript execution failed: error: {
"$err" : "not authorized for query on blog.system.namespaces",
"code" : 13
} at src/mongo/shell/query.js:L128
> db.auth("admin","pass")
Error: 18 { code: 18, ok: 0.0, errmsg: "auth fails" }
0
> use admin
switched to db admin
> db.auth("admin","pass")
1
> use blog
switched to db blog
> show collections
system.indexes
system.users
authors
posts
tagcloud
MongoDB 用户角色
目前,MongoDB 支持用户在其权限框架内可以拥有的以下角色:
- 读取—允许用户从给定的数据库中读取。
- readWrite 授予用户对给定数据库的读写权限。
- db admin—允许用户在给定的数据库中执行管理功能,例如创建或删除索引、查看统计数据或访问
system.profile集合。 - user admin—允许用户写入
system.users集合。有了该权限,您可以创建、删除和管理该数据库的用户。 - cluster admin—仅在
admin数据库中可用。授予对所有与分片和副本集相关的功能的完全管理权限。 - readany database—仅在
admin数据库中可用。授予对所有数据库的读取权限。 - readWriteAnyDatabase—仅在
admin数据库中可用。授予对所有数据库的读写权限。 - useradminany database—仅在
admin数据库中可用。授予 userAdmin 对所有数据库的权限。 - dbAdminAnyDatabase—仅在
admin数据库中可用。授予 dbAdmin 对所有数据库的权限。
委派凭据
如前所述,从 MongoDB 2.4 版本开始,可以拥有一个主用户,然后创建后续用户,这些用户使用主用户的凭证进行身份验证,这一特性称为创建委托凭证。假设我们在我们的foo数据库上创建用户tes t,如下所示:
> use foo
> db.addUser(user : "test", pwd: "password", roles: ["readWrite" ])
现在,假设我们想要在bar数据库上创建相同的测试用户。我们可以运行下面的命令(当然,作为一个拥有该数据库的用户管理员权限的用户)来创建我们的test用户,该用户将使用 foo 数据库的定义作为其密码:
>use bar
> db.system.users.insert{ user: "test", roles: ["read"], userSource: "foo"}
请注意,该用户仅被授予只读权限“read”这是因为授予 bar 上的测试用户的访问权限仍然基于这个 bar 用户的凭证。我们只是从foo数据库中获取我们需要的其余细节(即密码)。通过使用委派凭据,您可以创建一个单一位置来更新所有用户的用户名和密码。
更改用户的凭据
更改用户的访问权限或密码很容易。通过再次执行addUser()函数可以做到这一点,这将导致 MongoDB 更新现有的用户记录。从技术上讲,您可以使用任何普通的数据操作命令来更改用户的记录;但是,只有addUser()函数可以创建密码字段。
无论如何,您可以通过列出其内容来了解addUser()是如何工作的:
$mongo
>use admin
> db.addUser
function () {
if (arguments.length == 0) {
throw Error("No arguments provided to addUser");
}
if (typeof arguments[0] == "object") {
this._addUser.apply(this, arguments);
} else {
this._addUserV22.apply(this, arguments);
}
}
addUser()只是 JavaScript 中定义的一个函数。如果您想创建一个允许您向数据库添加用户的 web 表单,或者想从另一个凭证源将用户全部导入到系统中,了解密码的构造方式是非常有用的。
大多数mongo控制台功能可以以这种方式列出,使您能够检查它们如何工作的细节。
添加只读用户
addUser()函数包括一个附加参数,允许您创建一个只有只读权限的用户。如果作为新创建的用户进行身份验证的进程试图做任何会导致数据库内容发生变化的事情,MongoDB 客户端将抛出异常。以下示例为用户提供了对数据库的访问权限,以便进行状态监控或报告:
$mongo
>use admin
switched to db admin
>db.addUser(user : "admin", pwd: "pass", roles: [ "read" ])
1
>use blog
switched to db blog
>db.addUser("shadycharacter","shadypassword", true)
删除用户
要从数据库中删除用户,只需对集合使用普通的remove()函数。以下示例删除刚刚添加的用户;请注意,在删除用户之前,您必须针对admin数据库进行身份验证:
$mongo
>use admin
switched to db admin
> db.auth("admin","pass")
1
>use blog
switched to db blog
>db.removeUser("shadycharacter")
在 PHP 应用中使用认证连接
在第 4 章中,您看到了如何用 PHP 创建到 MongoDB 服务器的连接。一旦在服务器上启用了身份验证,PHP 应用也必须提供凭证,然后才能对服务器执行命令。以下简单示例显示了如何打开到数据库的已验证连接:
<?php
// Establish the database connection
$connection = new Mongo();
$db = $connection->selectDB(“admin”);
$result = $db->authenticate(“admin”, “pass”);
if(!$result[‘ok’]){
// Your Error handling here
die(“Authentication Error: {$result[‘errmsg’]}”);
}
// Your code here
// Close the database connection
$connection->close();
?>
管理服务器
作为管理员,您必须确保 MongoDB 服务器平稳可靠地运行。
您必须定期调整服务器以获得最佳性能,或者重新配置它们以更好地适应您的操作环境。为此,您需要熟悉一些使您能够管理和控制服务器的过程。
启动服务器
大多数现代 Linux 发行版现在都包括一组用于管理服务的/etc/init.d脚本。如果您使用 MongoDB 站点上的一个分发包安装了您的 MongoDB 服务器(参见第 2 章中关于这些包的更多信息),那么用于管理您的服务器的init.d脚本已经安装好了。
您可以在 Ubuntu、Fedora、CentOS 和 RedHat 上使用service命令来启动、停止和重启服务器,如下例所示:
$sudo service mongodb start
mongodb start/running, process 3474
$sudo service mongodb stop
mongodb stop/waiting
$sudo service mongodb restart
mongodb start/running, process 3474
如果没有可用的初始化脚本,可以通过打开终端窗口,然后键入以下命令来手动启动 MongoDB 服务器:
$ mongod
Fri May 24 15:06:20.475 [initandlisten] MongoDB starting : pid=97163 port=27017 dbpath=/var/lib/mongodb 64-bit host=Pixl.local
Fri May 24 15:06:20.475 [initandlisten] db version v2.5.1-pre
Fri May 24 15:06:20.475 [initandlisten] git version: 704dc4fdf5248077c53271f249260478d6c56cd3
Fri May 24 15:06:20.475 [initandlisten] build info: Darwin bs-osx-106-x86-64-1.local 10.8.0 Darwin Kernel Version 10.8.0: Tue Jun 7 16:33:36 PDT 2011; root:xnu-1504.15.3∼1/RELEASE_I386 i386 BOOST_LIB_VERSION=1_49
Fri May 24 15:06:20.475 [initandlisten] allocator: system
Fri May 24 15:06:20.475 [initandlisten] options: {}
Fri May 24 15:06:20.479 [initandlisten] journal dir=/data/db/journal
Fri May 24 15:06:20.479 [initandlisten] recover : no journal files present, no recovery needed
Fri May 24 15:06:20.547 [websvr] admin web console waiting for connections on port 28017
服务器将显示所有正在进行的连接,以及其他可以用来监控服务器工作情况的信息。
要以手动模式终止服务器,只需键入^C;这将导致服务器彻底关闭。
如果您不提供配置文件,那么 MongoDB 将使用默认数据库路径/data/db启动,并使用默认端口 27017 (mongodb)和 28017(管理界面)绑定到所有网络 IP,如下例所示:
$ mkdir -p /data/db
$ mongod
mongod --help for help and startup options
...
Sun Jun 13 13:38:00 waiting for connections on port 27017
Sun Jun 13 13:38:00 web admin interface listening on port 28017
^C
Sun Jun 13 13:40:26 got kill or ctrl c signal 2 (Interrupt), will terminate after current cmd ends
...
Sun Jun 13 13:40:26 dbexit: really exiting now
重新配置服务器
MongoDB 提供了三种配置服务器的主要方法。首先,您可以将命令行选项与mongod服务器守护进程结合使用。其次,您可以通过加载配置文件来实现。第三,您可以使用setParameter命令更改大多数设置。例如,我们可以用下面的命令将logLevel改回默认值 0:
> db.adminCommand( {setParameter:1, logLevel:0 } )
大多数预打包的 MongoDB 安装程序使用后一种方法,使用通常存储在 Unix/Linux 系统上的/etc/mongodb.conf中的文件。
您可以通过编辑该文件并重新启动服务器来更改服务器的配置。该文件的内容如下所示:
# mongodb.conf
dbpath=/var/lib/mongodb
logpath=/var/log/mongodb/mongodb.log
logappend=true
auth = false
#enable the rest interface
rest =true
您可以通过删除选项前面的#代码并根据需要设置其值来启用选项,因为任何以#开头的行都被认为是“注释”,因此会被忽略。
在配置文件中放置以下任何选项值都等同于指定
--<optionname> <optionvalue>
启动 MongoDB 时在命令行上:
dbpath:表示 MongoDB 将存储您的数据的位置;您应该确保它位于足够大的快速存储卷上,以支持您的数据库大小。logpath:表示 MongoDB 将在其中存储日志的文件。放这个的标准地方是/var/logs/mongodb/mongodb.log;你需要使用logrotate来旋转这个日志文件,防止它填满你的服务器驱动器。logappend:将该选项设置为false会导致每次启动 MongoDB 时清除日志文件。将此选项设置为true会将所有日志条目附加到任何现有日志文件的末尾。auth:启用或禁用 MongoDB 服务器上的认证模式;有关身份验证的更多信息,请参见本章前面的讨论。rest:启用或禁用 MongoDB 的rest接口。如果您想使用基于 web 的状态显示中的链接来显示附加信息,那么您必须启用这个接口,但是不建议生产服务器使用这个接口,因为所有这些信息都应该可以通过 Mongo shell 获得。
获取服务器的版本
你可以用数据库。version()获取服务器内部版本和版本信息的函数。此信息对于确定是否需要升级或向支持论坛报告问题非常有用。以下代码片段显示了如何使用该命令:
$mongo
> use admin
switched to db admin
> db.version()
version: 2.5.1-pre-
获取服务器的状态
MongoDB 提供了一种简单的方法来确定服务器的状态。
Note
请记住,如果您正在使用auth,您的用户将需要权限来运行这些命令。
以下示例显示了返回的信息,包括服务器正常运行时间、最大连接数等信息:
$mongo
> db.serverStatus()
{
"host" : "Pixl.local",
"version" : "2.5.1-pre-",
"process" : "mongod",
"pid" : 3737,
"uptime" : 44,
"uptimeMillis" : NumberLong(43035),
"uptimeEstimate" : 39,
"localTime" : ISODate("2013-05-25T12:38:34.015Z"),
"asserts" : {
"regular" : 0,
"warning" : 0,
"msg" : 0,
"user" : 1,
"rollovers" : 0
},
"connections" : {
"current" : 1,
"available" : 2047,
"totalCreated" : NumberLong(1)
},
"cursors" : {
"totalOpen" : 0,
"clientCursors_size" : 0,
"timedOut" : 0
},
"globalLock" : {
"totalTime" : NumberLong(43035000),
"lockTime" : NumberLong(48184),
"currentQueue" : {
"total" : 0,
"readers" : 0,
"writers" : 0
},
},
"locks" : {
"admin" : {
"timeLockedMicros" : {
"r" : NumberLong(54),
"w" : NumberLong(0)
},
"timeAcquiringMicros" : {
"r" : NumberLong(2190),
"w" : NumberLong(0)
}
},
"local" : {
"timeLockedMicros" : {
"r" : NumberLong(45),
"w" : NumberLong(6)
},
"timeAcquiringMicros" : {
"r" : NumberLong(7),
"w" : NumberLong(1)
}
},
...
},
"network" : {
"bytesIn" : 437,
"bytesOut" : 6850,
"numRequests" : 7
},
"opcounters" : {
"insert" : 1,
"query" : 6,
"update" : 0,
"delete" : 0,
"getmore" : 0,
"command" : 7
},
...
"mem" : {
"bits" : 64,
"resident" : 37,
"virtual" : 3109,
"supported" : true,
"mapped" : 320,
"mappedWithJournal" : 640
},
"ttl" : {
"deletedDocuments" : NumberLong(0),
"passes" : NumberLong(0)
}
},
"ok" : 1
}
正如你所看到的,serverStatus输出了相当多的细节,上面是被截断的!您可以在opcounters和asserts部分找到该函数返回的信息中最重要的两个部分。
opcounters部分显示了针对数据库服务器执行的每种操作的数量。对于您的特定应用,您应该很清楚这些计数器的正常余额是由什么组成的。如果这些计数器开始超出正常比率,那么这可能是您的应用有问题的早期警告。
例如,所示的概要文件具有极高的插入/读取比率。这对于日志记录应用来说是正常的;然而,对于一个博客应用,它可能表明要么是一个垃圾邮件正在点击您的“评论”部分,要么是一个导致数据库写入的 URL 模式正在被搜索引擎蜘蛛重复抓取。在这种情况下,是时候在你的评论表单上放一个验证码,或者在你的robots.tx file中屏蔽特定的 URL 模式了。
asserts部分显示了已经抛出的服务器和客户机异常或警告的数量。如果这样的异常或警告开始迅速增加,那么是时候好好检查一下服务器的日志文件,看看问题是否正在发展。大量的断言也可能表明数据库中的数据有问题,您应该检查 MongoDB 实例的日志文件,以确认这些断言的性质,以及它们是否表明正常的“用户断言”,这表示类似重复键违规或更紧迫的问题。
关闭服务器
如果您已经从一个包中安装了 MongoDB 服务器,那么您可以使用操作系统的服务管理脚本来关闭服务器。例如,Ubuntu、Fedora、CentOS 和 RedHat 允许您通过发出以下命令来关闭服务器:
$sudo service mongod stop
您也可以从mongo控制台关闭服务器:
$mongo
>use admin
>db.shutdownServer()
您可以使用 Posix 进程管理命令来终止服务器,或者使用SIG_TERM(-15) or SIG_INT(-2)信号来关闭服务器。
如果且仅当服务器无法响应这两种方法时,您可以使用以下命令:
$sudo killall -15 mongod
Warning
您不能使用SIG_KILL(-9)信号来终止服务器,因为这可能会导致数据库损坏,并且您可能需要修复服务器。
这可能是因为您有一个特别活跃的服务器,它有很多写入活动,并且您已经重新配置了该服务器,因此它有很大的同步延迟。如果是这种情况,那么服务器可能不会立即响应终止请求,因为它正在将所有内存中的更改写到磁盘上。一点点耐心在这里大有帮助。
使用 MongoDB 日志文件
默认情况下,MongoDB 将其整个日志输出写入stdout;但是,您可以使用前面描述的logpath选项将日志输出重定向到一个文件。
您可以使用日志文件的内容来发现问题,例如来自单个计算机的过多连接以及其他可能表明应用逻辑或数据有问题的错误消息。
验证和修复您的数据
如果您的服务器意外重启或者您的 MongoDB 服务器由于任何原因崩溃,您的数据可能会处于损坏或不完整的状态。
以下是一些表明您的数据已经受损的迹象:
- 您的数据库服务器拒绝启动,声称数据文件已损坏。
- 您开始在服务器日志文件中看到断言,或者在使用
db.serverStatus()命令时看到高断言计数。 - 您从查询中得到奇怪或意想不到的结果。
- 收藏数量的记录与你的期望不符。
这些迹象中的任何一个都可能表明您的应用有问题,或者更令人担忧的是,您的数据损坏或不一致。
幸运的是,MongoDB 附带了帮助您修复或恢复数据库服务器的工具。尽管如此,您仍然可能会丢失一些数据,所以请记住黄金法则,确保您有一个良好的数据备份或一个复制从属。
修复服务器
在您启动服务器修复过程之前,您必须意识到运行repair命令是一项成本高昂的操作,可能会花费大量时间,并且它需要的空间是 MongoDB 数据文件所占用空间的两倍,因为所有数据都被克隆到新文件中并完全重新创建,这实际上是所有数据文件的重建。这是使用副本集的最佳论据之一:如果您必须让一台机器脱机进行修复,您不必完全停止副本集为您的客户机提供服务。
要启动修复过程,只需使用手动服务器启动过程(如本章前面所述)。但是,这一次您需要将--repair选项添加到命令的末尾,如下例所示:
$ mongod --dbpath /data/db --repair
Wed Sep 18 21:21:21.364 [initandlisten] MongoDB starting : pid=5973 port=27017 dbpath=/data/db 64-bit host=Pixl.local
Wed Sep 18 21:21:21.364 [initandlisten]
Wed Sep 18 21:21:21.364 [initandlisten] ** WARNING: soft rlimits too low. Number of files is 256, should be at least 1000
Wed Sep 18 21:21:21.364 [initandlisten] db version 2.5.1-pre-
Wed Sep 18 21:21:21.364 [initandlisten] git version: 704dc4fdf5248077c53271f249260478d6c56cd3
Wed Sep 18 21:21:21.364 [initandlisten] build info: Darwin bs-osx-106-x86-64-2.10gen.cc 10.8.0 Darwin Kernel Version 10.8.0: Tue Jun 7 16:32:41 PDT 2011; root:xnu-1504.15.3∼1/RELEASE_X86_64 x86_64 BOOST_LIB_VERSION=1_49
Wed Sep 18 21:21:21.364 [initandlisten] allocator: system
Wed Sep 18 21:21:21.364 [initandlisten] options: { dbpath: "/data/db", repair: true }
Wed Sep 18 21:21:21.367 [initandlisten] build index test.system.users { user: 1, userSource: 1 }
Wed Sep 18 21:21:21.368 [initandlisten] build index done. scanned 1 total records. 0.001 secs
Wed Sep 18 21:21:21.368 [initandlisten] ****
Wed Sep 18 21:21:21.368 [initandlisten] ****
Wed Sep 18 21:21:21.368 [initandlisten] need to upgrade database test with pdfile version 4.5, new version: 4.5
Wed Sep 18 21:21:21.368 [initandlisten] starting upgrade
Wed Sep 18 21:21:21.368 [initandlisten] test repairDatabase test
Wed Sep 18 21:21:21.369 [FileAllocator] allocating new datafile /data/db/_tmp_repairDatabase_0/test.ns, filling with zeroes...
Wed Sep 18 21:21:21.369 [FileAllocator] creating directory /data/db/_tmp_repairDatabase_0/_tmp
Wed Sep 18 21:21:21.389 [FileAllocator] done allocating datafile /data/db/_tmp_repairDatabase_0/test.ns, size: 16MB, took 0.02 secs
Wed Sep 18 21:21:21.389 [FileAllocator] allocating new datafile /data/db/_tmp_repairDatabase_0/test.0, filling with zeroes...
Wed Sep 18 21:21:21.583 [FileAllocator] done allocating datafile /data/db/_tmp_repairDatabase_0/test.0, size: 64MB, took 0.193 secs
Wed Sep 18 21:21:21.583 [FileAllocator] allocating new datafile /data/db/_tmp_repairDatabase_0/test.1, filling with zeroes...
Wed Sep 18 21:21:21.586 [initandlisten] build index test.foo { _id: 1 }
Wed Sep 18 21:21:21.661 [initandlisten] fastBuildIndex dupsToDrop:0
Wed Sep 18 21:21:21.661 [initandlisten] build index done. scanned 1 total records. 0.074 secs
Wed Sep 18 21:21:21.661 [initandlisten] build index test.system.users { user: 1, userSource: 1 }
Wed Sep 18 21:21:21.662 [initandlisten] fastBuildIndex dupsToDrop:0
Wed Sep 18 21:21:21.662 [initandlisten] build index done. scanned 0 total records. 0 secs
Wed Sep 18 21:21:21.662 [initandlisten] build index test.system.users { _id: 1 }
Wed Sep 18 21:21:21.663 [initandlisten] fastBuildIndex dupsToDrop:0
Wed Sep 18 21:21:21.663 [initandlisten] build index done. scanned 1 total records. 0 secs
Wed Sep 18 21:21:22.002 [FileAllocator] done allocating datafile /data/db/_tmp_repairDatabase_0/test.1, size: 128MB, took 0.418 secs
Wed Sep 18 21:21:22.018 [initandlisten] finished checking dbs
Wed Sep 18 21:21:22.018 dbexit:
Wed Sep 18 21:21:22.018 [initandlisten] shutdown: going to close listening sockets...
Wed Sep 18 21:21:22.018 [initandlisten] shutdown: going to flush diaglog...
Wed Sep 18 21:21:22.018 [initandlisten] shutdown: going to close sockets...
Wed Sep 18 21:21:22.018 [initandlisten] shutdown: waiting for fs preallocator...
Wed Sep 18 21:21:22.018 [initandlisten] shutdown: closing all files...
Wed Sep 18 21:21:22.018 [initandlisten] closeAllFiles() finished
Wed Sep 18 21:21:22.018 [initandlisten] shutdown: removing fs lock...
Wed Sep 18 21:21:22.018 dbexit: really exiting now
在本例中,repair检测到admin数据库可能是在旧版本的 MongoDB 下创建的,它需要升级其存储格式以匹配当前运行的服务器。
Note
运行带有--repair选项的mongod实用程序后,服务器退出是正常的;要使它重新联机,只需再次启动它,无需指定--repair选项。
修复过程完成后,您应该能够照常启动服务器,然后从备份中还原任何丢失的数据。
如果您试图修复一个大型数据库,您可能会发现您的驱动器耗尽了磁盘空间,因为 MongoDB 可能需要在与数据相同的驱动器上创建一个数据库文件的临时副本(参见前面示例中的.../$tmp_repairDatabase_0/..目录)。
为了克服这个潜在的问题,MongoDB 修复实用程序支持一个名为--repairpath的附加命令行参数。您可以使用此参数指定一个驱动器,该驱动器具有足够的空间来保存它在重建过程中创建的临时文件,如下例所示:
$ mongod -f /etc/mongodb.conf --repair --repairpath /mnt/bigdrive/tempdir
验证单个集合
有时,您可能会怀疑正在运行的服务器上的数据有问题。在这种情况下,您可以使用 MongoDB 附带的一些工具来帮助您确定有问题的服务器是否已经损坏。
您可以使用validate选项来验证数据库中集合的内容。下一个例子展示了如何对包含一百万条记录的集合运行validate选项:
$mongo
> use blog
switched to db blog
>db.posts.ensureIndex({Author:1})
> db.posts.validate()
{
"ns" : "blog.posts",
"firstExtent" : "0:f000 ns:blog.posts",
"lastExtent" : "0:2b9000 ns:blog.posts",
"extentCount" : 6,
"datasize" : 6717520,
"nrecords" : 29997,
"lastExtentSize" : 8388608,
"padding" : 1,
"firstExtentDetails" : {
"loc" : "0:f000",
"xnext" : "0:11000",
"xprev" : "null",
"nsdiag" : "blog.posts",
"size" : 8192,
"firstRecord" : "0:f0b0",
"lastRecord" : "0:10e50"
},
"lastExtentDetails" : {
"loc" : "0:2b9000",
"xnext" : "null",
"xprev" : "0:b9000",
"nsdiag" : "blog.posts",
"size" : 8388608,
"firstRecord" : "0:2b90b0",
"lastRecord" : "0:6ec830"
},
"deletedCount" : 4,
"deletedSize" : 3983552,
"nIndexes" : 2,
"keysPerIndex" : {
"blog.posts.$_id_" : 29997,
"blog.posts.$Author_1" : 29997
},
"valid" : true,
"errors" : [ ],
"warning" : "Some checks omitted for speed. use {full:true} option to do more thorough scan.",
"ok" : 1
}
前面的示例大约需要 30 秒钟才能完成。默认情况下,validate选项检查数据文件和索引,并在收集完成时提供一些关于收集的统计信息。该选项会告诉您数据文件或索引是否有任何问题,但不会检查每个文档的正确性。如果检查每个文档是您想要的,那么您可以运行(如输出中所建议的)带有{full: true}选项的validate,这是通过向函数调用添加true参数来调用的,如下所示:db.posts.validate(true)。
如果您有一个非常大的数据库,并且您只想验证索引,那么您也可以使用validate选项。在当前版本(2.6.0)中,没有用于此的 shell helper 命令。但是这并不是一个障碍,因为您可以使用runCommand选项轻松完成这个索引验证:
$mongo
>use blog
>db.runCommand({validate:"posts", scandata:false})
在这种情况下,服务器不会扫描数据文件;相反,它仅仅报告存储的关于集合的信息。
修复集合验证错误
如果在您的集合上运行验证发现了一个错误,这将在 validate 文档的errors部分中记录,您有几个选项来修复数据。同样,拥有良好备份的重要性怎么强调都不为过。在直接开始恢复您的备份之前,您应该查看 MongoDB 实例的日志,看看是否有关于错误性质的任何附加信息;如果是这样,这应该会告诉你下一步该怎么做。
修复集合的索引
如果验证过程显示索引被损坏,使用reIndex()函数重新索引受影响的集合。在下面的例子中,您使用reIndex()函数来重新索引博客的posts集合,之前您已经向该集合添加了author索引:
$mongo
>use blog
> db.posts.reIndex()
{
"nIndexesWas" : 2,
"msg" : "indexes dropped for collection",
"nIndexes" : 2,
"indexes" : [
{
"key" : {
"_id" : 1
},
"ns" : "blog.posts",
"name" : "_id_"
},
{
"key" : {
"Author" : 1
},
"ns" : "blog.posts",
"name" : "Author_1"
}
],
"ok" : 1
}
MongoDB 服务器将删除集合上的所有当前索引并重新构建它们;但是,如果您使用 database repair选项,它还会对数据库中的所有集合运行reIndex()函数。
修复集合的数据文件
修复数据库中所有数据文件的最佳——也是最危险的——方法是使用服务器的--repair选项或 db。repairDatabase()shell 中的命令。后者修复单个数据库中的所有集合文件,然后重新索引所有已定义的索引。然而,repairDatabase()并不是一个适合在实时服务器上运行的函数,因为它会在重建数据文件时阻止对数据的任何请求。这导致在数据库修复完成时阻止所有读取和写入。下面的代码片段显示了使用repairDatabase()函数的语法:
$mongo
>use blog
>db.repairDatabase()
{ "ok" : 1 }
Warning
MongoDB 的修复是一个蛮力选项。它试图修复和重建您的数据结构和索引。它通过尝试从磁盘读取并重建整个数据结构来实现这一点。如果可能,您应该尝试从备份中恢复;repairDatabase()只应作为最后手段使用。
压缩集合的数据文件
由于 MongoDB 在内部分配数据文件的方式,您可能会遇到俗称的“瑞士奶酪”,这意味着在磁盘数据结构中留下了数据存储空间的一小部分空白。这可能是一个问题,因为这意味着您的数据文件有很大一部分未被使用。虽然修复以重建整个数据结构可能会有所帮助,但可能会有其他意想不到的后果。compact命令将对现有数据文件中给定集合的数据结构进行分片整理和重组,但它不会恢复磁盘空间。
$mongo
>use blog
> db.runCommand({compact:"posts"})
{ "ok" : 1 }
升级 MongoDB
有时,新版本的 MongoDB 会要求您升级数据库文件的格式。MongoDB,Inc .的团队意识到了在正在运行的生产服务上运行升级所带来的影响(包括导致的停机时间);但是,有时为了支持大量需求的新功能,需要进行升级。
Warning
在尝试任何升级过程之前,对您的数据进行完整备份是非常重要的。除此之外,您应该经常查看发行说明,这些说明可从 http://docs.mongodb.org/manual/release-notes/ 获得。
MongoDB 的开发者试图预测升级过程中可能出现的每一个问题;然而,你也必须采取措施保护自己。升级通常会以新的格式重写系统中的每一条数据,这意味着即使是过程中最轻微的问题也会带来灾难性的后果。
以下列表指导您完成升级数据库服务器所需的正确步骤:
Back up your data and make sure that the backup is viable. If possible, restore the backup to another server and verify that it’s OK. Stop your application or divert it to another server. Stop your MongoDB server. Upgrade the code of your MongoDB server to the desired version. Use the shell to perform initial sanity checks on the data. If anything looks suspicious, use the validation tools to check the data. Re-enable your application when you are satisfied that everything looks OK. Test your application carefully before reopening the service or diverting traffic back to this server.
MongoDB 的滚动升级
拥有副本集的一个重要特性是它可以用于执行滚动升级。这种方法旨在最大限度地减少像这样的大型变更所带来的潜在停机时间和影响。除了遵循下面概述的流程之外,您应该始终进行备份,并在您的非生产环境中进行测试。完成尽职调查以确保系统可恢复后,您可以遵循以下流程:
Stop and perform an upgrade for each secondary one at a time. Run the rs.stepDown() command on the primary. One of the upgraded secondaries will step into place as primary. Upgrade the primary.
监控 MongoDB
MongoDB 发行版包含一个名为mongostat的简单状态监控工具。该工具主要用于提供服务器上发生的事情的简单概述(见图 9-5 )。

图 9–5。
Monitoring the status of MongoDB with the mongostat utility
这个工具产生的统计数据并不广泛,但是它们确实提供了 MongoDB 安装中正在发生的事情的一个很好的概述。例如,这个显示可以让您看到数据库操作的执行频率、索引命中率,以及应用在等待释放数据库锁时被阻塞的时间。
感兴趣的主要列是前六列,它们显示了mongod服务器处理某些操作(例如,插入或查询)的速度。在诊断安装问题时,值得关注的其他列包括:
Pagefaults:表示当您的 MongoDB 实例需要从磁盘读取数据以完成查询时。这通常是次优性能的一个指标,日常操作通常需要的所有数据都无法保存在 MongoDB 可用的 RAM 中。您应该检查是否有任何可能扫描所有文档而不使用索引的查询,或者您可能需要转移到具有更多可用 RAM 的服务器。queues:表示排队等待执行的操作数。由于 MongoDB 允许一个写者(插入、更新和删除)和许多读者(发现),这可能导致读查询被性能差的写阻塞的情况。更糟糕的是,您可能会遇到这样的情况:一个性能不佳的写操作阻塞了多个读写操作。检查哪些查询可能会阻止其他查询的执行。% locked:显示给定集合的写锁被解除的时间百分比。此处非常高的数字表示您正在执行一个或多个几乎在整个时间窗口内运行的写操作。高的% locked可能会影响所有查询的性能。检查您的写入性能是否很差,或者您的系统是否存在页面错误,这可能表明需要更多的 RAM。这也可能与模式问题有关,比如文档中非常大的数组,请记住,在 MongoDB 2.2 之前,锁定是在每个实例级别上进行的,因此升级可能有助于通过最近的并发性改进来降低该值。
ROLLING YOUR OWN STAT MONITORING TOOL
由mongostat提供的许多信息与您可以从db.serverStatus()呼叫中获得的信息相同。创建一个使用这个 API 每隔几秒钟轮询一次服务器,然后将结果放入 MongoDB 集合的服务并不是一件难事。
一些索引、一些精心制作的查询和一个图形包的应用将使您能够使用这样一个简单的实时监控器来生成历史日志。
MongoDB 还提供了许多第三方适配器,允许您使用常见的开源或商业监控系统,包括 Nagios、Ganglia 和 Cacti 等工具。如前所述,MongoDB 手册在其网站上包括一个页面,该页面分享了 MongoDB 可用的监控接口的最新信息(有关该主题的更多信息,请参见 http://docs.mongodb.org/manual/administration/monitoring/ )。
使用 MongoDB 管理服务(MMS)
到目前为止讨论的大多数统计信息也可以通过 MongoDB 管理服务(也称为 MMS)获得。MMS 是 MongoDB,Inc .提供的一种监控服务,它提供了一个可以安装在本地机器上的代理。安装后,您可以通过 MMS 网页添加您的服务器,以便指示代理监控它们。一旦监控开始,您就可以深入到特定的主机,查看 MongoDB 实例的性能统计图表。您可以监控从单个 MongoDB 实例到副本集,直到完整的分片集群,包括配置服务器和 MongoS。MMS 还具有查看这些组的所有单个成员或查看每个组的汇总统计数据的功能。然后,您可以根据您的特定性能需求或 MongoDB 实例中发生的事件来配置发送给您的警报。您可以注册彩信at mms.mongodb.com,我们强烈建议您这样做。没有什么比深入了解每个 MongoDB 节点的性能更强大的了(参见图 9-6 中的例子)。

图 9–6。
Viewing statistics via MMS
摘要
保持 MongoDB 安装平稳运行通常只需要很少的努力。在这一章中,您已经看到了如何使用 MongoDB 发行版提供的工具来管理和维护您的系统,以及掌握任何可能出现的问题。
通过学习本章,您现在应该掌握了备份、恢复、升级和监控 MongoDB 实例的技能。您应该熟悉诸如 mongodump 和 mongorestore 之类的工具,知道如何只从某些集合中导入和导出数据,并且熟悉许多可以用来从 MongoDB 实例中导出性能和使用统计数据的管理命令。
最后,必须强调的是(从前面所说的一切中,您可能已经领会到了这一点),从本章中吸取的最重要的教训是:作为数据库系统的管理员,您的首要责任是确保为您的数据提供一个可靠的备份和恢复方案。
十、最优化
Abstract
一位不知名的 Twitter 用户半开玩笑地说:“如果一个 MongoDB 查询运行时间超过 0 毫秒,那么一定有问题。”这是该产品在 2009 年首次亮相时的典型反响。
一位不知名的 Twitter 用户半开玩笑地说:“如果一个 MongoDB 查询运行时间超过 0 毫秒,那么一定有问题。”这是该产品在 2009 年首次亮相时的典型反响。
现实情况是 MongoDB 速度非常快。但是如果您给它错误的数据结构,或者您没有用正确的索引设置集合,MongoDB 可能会像任何数据存储系统一样显著变慢。
MongoDB 还包含一些高级特性,这些特性需要一些调整才能以最佳效率运行。
数据模式的设计也会对性能产生很大的影响;在这一章中,我们将研究一些技术来将您的数据塑造成一种最大限度地利用 MongoDB 的优势并最小化其劣势的形式。
在我们研究如何提高运行在服务器上的查询的性能或者优化数据结构的方法之前,我们先来看看 MongoDB 如何与运行它的硬件进行交互,以及影响性能的因素。然后我们看一下索引,如何使用它们来提高查询的性能,以及如何分析 MongoDB 实例来确定哪些查询性能不好。
优化您的服务器硬件性能
通常,对数据库服务器进行最快速、最便宜的优化是调整运行它的硬件的大小。如果数据库服务器内存太少或使用慢速驱动器,会严重影响数据库性能。虽然这些限制中的一些对于开发环境可能是可接受的,在开发环境中,服务器可能运行在开发者的本地工作站上,但是它们对于生产应用可能是不可接受的,在生产应用中,必须小心计算正确的硬件配置以实现最佳性能。
了解 MongoDB 如何使用内存
MongoDB 使用内存映射文件 I/O 来访问其底层数据存储。这种文件 I/O 方法有一些您应该知道的特征,因为它们会影响运行它的操作系统(OS)的类型和您安装的内存量。
内存映射文件的第一个显著特征是,在现代 64 位操作系统上,可以管理的最大文件大小在 Linux 上约为 128 TB(Linux 虚拟内存地址限制),在 Windows 上约为 8TB(启用日志记录时为 4TB ),因为它对内存映射文件有限制。在 32 位操作系统上,您只能使用 2GB 的数据,因此不建议您使用 32 位操作系统,除非运行小型开发环境。
第二个显著的特征是,内存映射文件使用操作系统的虚拟内存系统,根据需要将数据库文件的所需部分映射到 RAM 中。这可能会给人留下一种稍微令人担忧的印象,即 MongoDB 正在耗尽系统的所有 RAM。事实并非如此,因为 MongoDB 将与其他应用共享虚拟地址空间。并且操作系统将在需要时释放内存给其他进程。使用空闲内存总量作为过度内存消耗的指标并不是一个好的做法,因为一个好的操作系统将确保只有很少或没有“空闲”内存。通过缓存或缓冲磁盘 I/O,所有昂贵的内存都被充分利用。空闲内存是浪费的内存。
通过提供适量的内存,MongoDB 可以将更多需要的数据映射到内存中,从而减少对昂贵的磁盘 I/O 的需求。
一般来说,给 MongoDB 的内存越多,它的运行速度就越快。然而,如果您有一个 2GB 的数据库,那么添加超过 2–3GB 的内存不会有太大的区别,因为整个数据库无论如何都将位于 RAM 中。
了解工作集大小
现在,我们需要讨论与 MongoDB 实例的性能调优相关的更复杂的事情之一,即工作集大小。这个大小表示存储在 MongoDB 实例中“在常规使用过程中”将被访问的数据量光是这句话就应该告诉你,这是一个主观的衡量标准,很难得到一个准确的值。
尽管很难量化,但是理解工作集大小的影响将有助于您更好地优化 MongoDB 实例。主要原则是,对于大多数安装,只有一部分数据需要作为常规操作的一部分进行访问。了解您将定期处理的数据部分使您能够正确调整硬件的大小,从而提高性能。
选择正确的数据库服务器硬件
普遍存在转向低功耗(能源)系统托管服务的压力。然而,许多低功耗服务器使用膝上型电脑或笔记本电脑组件来实现较低的功耗。不幸的是,质量较低的服务器硬件可以使用特别便宜的磁盘驱动器。这种驱动器不适合重型服务器应用,因为它们的磁盘转速较低,从而降低了与驱动器之间的数据传输速率。此外,请确保使用声誉良好的供应商,即您信任的供应商,来组装针对服务器操作进行了优化的系统。还值得一提的是,固态硬盘等更快、更现代的驱动器已经上市,这将大幅提升性能。如果可以安排的话,MongoDB,Inc .团队建议使用 RAID10 来实现性能和冗余。对于那些在云中的人来说,从 Amazon 获得像预配 IOPS 这样的东西是提高磁盘性能和可靠性的一个很好的方法。
如果您计划使用复制或任何需要通过网络连接进行读取的频繁备份系统,您应该考虑添加一个额外的网卡并形成一个单独的网络,以便服务器可以相互通信。这减少了用于将应用连接到服务器的网络接口上传输和接收的数据量,这也会影响应用的性能。
购买硬件时最需要注意的可能就是 RAM。因为 MongoDB 使用内存映射文件,所以有足够的空间将必要的数据保存在可以快速访问的地方是确保高性能的一个好方法。这是您可以链接到前面讨论的工作集概念的地方。在考虑购买硬件时,了解需要分配多少数据是关键。最后,记住你不需要出去买 512GB 的 RAM 安装在一台服务器上;您可以使用分片来分散数据负载(在第 12 章中讨论)。
评估查询性能
MongoDB 有两个主要的优化查询性能的工具:explain()和 MongoDB Profiler(分析器)。profiler 是一个很好的工具,可以用来查找那些性能不佳的查询,并选择候选查询进行进一步的检查,而explain()适合于调查单个查询,因此您可以确定它的性能如何。
熟悉 MySQL 的人可能也熟悉慢速查询日志的用法,它可以帮助您找到消耗大量时间的查询;MongoDB 使用分析器来提供这种能力。
MongoDB 分析器
MongoDB profiler 是一个工具,它记录满足触发条件的每个查询的统计信息和执行计划细节。通过使用--profile和--slowms选项启动 MongoD 进程,您可以在每个数据库上单独启用该工具,或者为所有数据库启用该工具(稍后将详细介绍这些值的含义)。这些选项也可以添加到你的mongodb.conf文件中,如果你是这样开始你的 MongoD 进程的话。
一旦启用了探查器,MongoDB 就会将一个文档插入到一个特殊的称为system.profile的有上限的集合中,该文档包含应用提交的每个查询的性能和执行细节信息。您可以使用此集合来检查使用标准集合查询命令记录的每个查询的详细信息。
system.profile集合被限制为最大 1024KB 的数据,这样探查器就不会用日志记录信息填充磁盘。这个限制应该足以捕获几千个甚至是最复杂的查询的概要文件。
Warning
启用探查器后,它会影响服务器的性能,因此让它在生产服务器上运行并不是一个好主意,除非您正在对一些观察到的问题进行分析。不要试图让它永久运行,以便为最近执行的查询提供一个窗口。
启用和禁用数据库探查器
打开 MongoDB 分析器很简单:
$mongo
>use blog
>db.setProfilingLevel(1)
禁用分析器也同样简单:
$mongo
>use blog
>db.setProfilingLevel(0)
MongoDB 还可以只为超过指定执行时间的查询启用探查器。以下示例只记录执行时间超过半秒的查询:
$mongo
>use blog
>db.setProfilingLevel(1,500)
如此处所示,对于分析级别 1,您可以提供以毫秒(ms)为单位的最大查询执行时间值。如果查询运行的时间超过了这个时间量,就会对其进行分析和记录。否则,它将被忽略。这提供了与 MySQL 的慢速查询日志相同的功能。
最后,通过将探查器级别设置为 2,可以为所有查询启用分析。
$mongo
>use blog
>db.setProfilingLevel(2)
查找慢速查询
system.profile集合中的典型记录如下所示:
> db.system.profile.find()
{
"op" : "query",
"ns" : "blog.system.profile",
"query" : {
},
"ntoreturn" : 0,
"ntoskip" : 0,
"nscanned" : 1,
"keyUpdates" : 0,
"numYield" : 0,
"lockStats" : {
"timeLockedMicros" : {
"r" : NumberLong(60),
"w" : NumberLong(0)
},
"timeAcquiringMicros" : {
"r" : NumberLong(4),
"w" : NumberLong(3)
}
},
"nreturned" : 1,
"responseLength" : 370,
"millis" : 12,
"ts" : ISODate("2013-05-18T05:40:27.106Z"),
"client" : "127.0.0.1",
"user" : ""
}
每个记录都包含字段,下面的列表概述了它们是什么以及它们的作用:
op:显示操作的类型;它可以是查询、插入、更新、命令或删除。query:正在运行的查询。- 运行此查询的完整名称空间。
ntoreturn:要返回的单据数量,nscanned:扫描返回本文档的索引条目数。ntoskip:跳过的文档数。keyUpdates:本次查询更新的索引键个数。numYields:该查询将其锁让给另一个查询的次数。lockStats:获取或读写该数据库的锁所花费的微秒数。nreturned:返回的文档数。responseLength:响应的字节长度。millis:执行查询所用的毫秒数。ts:以 UTC 格式显示时间戳,指示查询的执行时间。client:运行此查询的客户端的连接详细信息。user:运行此操作的用户。
因为system.profile集合只是一个普通的集合,所以您可以使用 MongoDB 的查询工具快速找到有问题的查询。
下一个示例查找执行时间超过 10ms 的所有查询。在这种情况下,您可以只在system.profile集合中查询millis >10的情况,然后按照执行时间降序排列结果:
> db.system.profile.find({millis:{$gt:10}}).sort({millis:-1})
{ "op" : "query", "ns" : "blog.system.profile", "query" : { }, "ntoreturn" : 0, "ntoskip" : 0, "nscanned" : 1, "keyUpdates" : 0, "numYield" : 0, "lockStats" : { "timeLockedMicros" : { "r" : NumberLong(60), "w" : NumberLong(0) }, "timeAcquiringMicros" : { "r" : NumberLong(4), "w" : NumberLong(3) } }, "nreturned" : 1, "responseLength" : 370, "millis" : 12, "ts" : ISODate("2013-05-18T05:40:27.106Z"), "client" : "127.0.0.1", "allUsers" : [ ], "user" : "" }
如果您还知道您的问题发生在特定的时间范围内,那么您可以使用ts字段添加查询词,将范围限制在所需的时间片。
增加配置文件集合的大小
如果您发现由于某种原因您的配置文件集合太小,您可以增加它的大小。
首先,您需要在希望增加其概要文件集合大小的数据库上禁用概要文件分析,以确保在执行此操作时没有任何内容写入其中:
$mongo
>use blog
>db.setProfilingLevel(0)
接下来,您需要删除现有的system.profile集合:
>db.system.profile.drop()
删除集合后,您现在可以使用createCollection命令创建新的 profiler 集合,并指定所需的字节大小。下面的示例创建一个上限为 50MB 的集合。它使用的表示法是将 50 字节转换为千字节,然后再转换为兆字节,每增加一次,就要乘以 1024:
>db.createCollection( "system.profile", { capped: true, size: 50 * 1024 * 1024 } )
{ "ok" : 1 }
有了新的更大的封顶集合,现在可以重新启用概要分析了:
>db.setProfilingLevel(2)
使用 explain()分析特定查询
如果您怀疑某个查询的性能不如预期,您可以使用explain()修饰符来查看 MongoDB 是如何执行查询的。
当您将explain()修饰符添加到查询中时,MongoDB 在执行时会返回一个描述查询如何处理的文档,而不是指向结果的光标。以下查询针对博客文章的数据库运行,表明该查询必须扫描 13,325 条记录,才能形成返回所有文章的游标:
$mongo
>use blog
> db.posts.find().explain()
{
"cursor" : "BasicCursor",
"isMultiKey" : false,
"n" : 13235,
"nscannedObjects" : 13235,
"nscanned" : 13235,
"nscannedObjectsAllPlans" : 13235,
"nscannedAllPlans" : 13235,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
},
"server" : "Pixl.local:27017"
}
您可以看到表10–1中列出的explain()返回的字段。
表 10–1。
Elements Returned by explain()
| 元素 | 描述 | | --- | --- | | `Cursor` | 指示为枚举结果而创建的游标的类型。通常,这是下列之一:`BasicCursor`,自然顺序阅读光标;一个`BtreeCursor`,它是一个索引光标;或者是一个`GeoSearchCursor`,这是一个使用地理空间索引的查询。 | | `indexBounds` | 指示索引查找中使用的最小/最大值。 | | `nScanned` | 指示为查找查询中的所有对象而扫描的索引条目数。 | | `nScannedObjects` | 指示被扫描的实际对象的数量,而不仅仅是它们的索引项。 | | `n` | 指示光标上的项数(即要返回的项数)。 | | `millis` | 指示执行查询所用的毫秒数。 | | `nscannedAllPlans` | 表示所有尝试计划的`nscanned`值。 | | `nscannedObjectsAllPlans` | 表示所有尝试计划的`nScannedObjects`值。 | | `scanAndOrder` | 一个 true/false 值,指示文档是否需要阅读才能排序,而不是利用索引。 | | `indexOnly` | 表示不需要扫描任何文档来返回查询结果;所有被查询和返回的字段都在一个索引上。 | | `nYields` | 该查询产生其读锁以允许执行写操作的次数。 | | `nChunkSkips` | 由于活动块迁移而跳过的文档数。 | | `server` | 执行该查询的服务器。 |使用 Profiler 和 explain()优化查询
现在让我们浏览一个真实的优化场景,看看我们如何使用 MongoDB 的 profiler 和explain()工具来修复一个真实应用的问题。
本章中讨论的例子是基于一个小的示例博客应用。该数据库具有获取与特定标签相关联的帖子的功能;在这种情况下,它是even标签。假设您已经注意到这个函数运行缓慢,因此您想确定是否有问题。
让我们从编写一个小程序开始,用数据填充前面提到的数据库,这样我们就可以运行查询,演示优化过程。
<?php
// Get a connection to the database
$mongo = new MongoClient();
$db=$mongo->blog;
// First let's get the first AuthorsID
// We are going to use this to fake a author
$author = $db->authors->findOne();
if(!$author){
die("There are no authors in the database");
}
for( $i = 1; $i < 10000; $i++){
$blogpost=array();
$blogpost['author'] = $author['_id'];
$blogpost['Title'] = "Completely fake blogpost number {$i}";
$blogpost['Message'] = "Some fake text to create a database of blog posts";
$blogpost['Tags'] = array();
if($i%2){
// Odd numbered blogs
$blogpost['Tags'] = array("blog", "post", "odd", "tag{$i}");
} else {
// Even numbered blogs
$blogpost['Tags'] = array("blog", "post", "even", "tag{$i}");
}
$db->posts->insert($blogpost);
}
?>
这个程序在blog数据库的authors集合中找到第一个作者,然后假装这个作者非常多产。它以作者的名义创建了 10,000 个虚假的博客帖子,所有这些都是在眨眼之间完成的。帖子读起来没什么意思;然而,它们被交替分配了odd和even标签。这些标签将用来演示如何优化一个简单的查询。
下一步是将程序保存为fastblogger.php,然后使用命令行 PHP 工具运行它:
$php fastblogger.php
接下来,您需要启用数据库概要分析器,您将使用它来确定是否可以改进示例的查询:
$ mongo
> use blog
switched to db blog
> show collections
authors
posts
...
system.profile
tagcloud
...
users
> db.setProfilingLevel(2)
{ "was" : 0, "slowms" : 100, "ok" : 1 }
现在稍等片刻,让命令生效,打开所需的集合,然后执行其他任务。接下来,您想要模拟让博客网站访问所有带有even标签的博客文章。通过执行站点可以用来实现此功能的查询来实现这一点:
$Mongo
use blog
$db.posts.find({Tags:"even"})
...
如果您在profiler集合中查询超过 5 毫秒的结果,您应该会看到类似这样的内容:
>db.system.profile.find({millis:{$gt:5}}).sort({millis:-1})
{ "op" : "query", "ns" : "blog.posts", "query" : { "tags" : "even" }, "ntoreturn" : 0, "ntoskip" : 0, "nscanned" : 19998, "keyUpdates" : 0, "numYield" : 0, "lockStats" : { "timeLockedMicros" : { "r" : NumberLong(12869), "w" : NumberLong(0) }, "timeAcquiringMicros" : { "r" : NumberLong(5), "w" : NumberLong(3) } }, "nreturned" : 0, "responseLength" : 20, "millis" : 12, "ts" : ISODate("2013-05-18T09:04:32.974Z"), "client" : "127.0.0.1", "allUsers" : [ ], "user" : "" }...
这里返回的结果显示,一些查询花费的时间超过 0 毫秒(记住本章开头的引文)。
接下来,您希望为第一个(也是性能最差的)查询重建查询,这样您就可以看到返回了什么。前面的输出表明性能差的查询正在查询blog.posts,查询词是{Tags:"even"}。最后,您可以看到这个查询花费了 15 毫秒来执行。
重建的查询如下所示:
>db.posts.find({Tags:"even"})
{ "_id" : ObjectId("4c727cbd91a01b2a14010000"), "author" : ObjectId("4c637ec8b8642fea02000000"), "Title" : "Completly fake blogpost number 2", "Message" : "Some fake text to create a database of blog posts", "Tags" : [ "blog", "post", "even", "tag2" ] }
{ "_id" : ObjectId("4c727cbd91a01b2a14030000"), "author" : ObjectId("4c637ec8b8642fea02000000"), "Title" : "Completly fake blogpost number 4", "Message" : "Some fake text to create a database of blog posts", "Tags" : [ "blog", "post", "even", "tag4" ] }
{ "_id" : ObjectId("4c727cbd91a01b2a14050000"), "author" : ObjectId("4c637ec8b8642fea02000000"), "Title" : "Completly fake blogpost number 6", "Message" : "Some fake text to create a database of blog posts", "Tags" : [ "blog", "post", "even", "tag6" ] }
...
这一结果应该不足为奇;创建这个查询的明确目的是演示如何查找和修复一个缓慢的查询。
目标是找出如何让查询运行得更快,所以使用explain()函数来确定 MongoDB 如何执行这个查询:
> db.posts.find({Tags:"even"}).explain()
{
"cursor" : "BasicCursor",
"isMultiKey" : false,
"n" : 14997,
"nscannedObjects" : 29997,
"nscanned" : 29997,
"nscannedObjectsAllPlans" : 29997,
"nscannedAllPlans" : 29997,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 27,
"indexBounds" : {
},
"server" : "Pixl.local:27017"
}
从这里的输出可以看出,查询没有使用任何索引。explain()函数显示该查询使用了一个“BasicCursor”,这意味着该查询只是对集合的记录进行简单的扫描。具体来说,它正在逐个扫描数据库中的所有记录,以找到标签(全部 9999 个标签);这个过程需要 27 毫秒。这听起来时间可能不长,但是如果您在网站的热门页面上使用这个查询,将会给磁盘 I/O 带来额外的负载,并给 web 服务器带来巨大的压力。因此,在创建页面时,该查询会导致与 web 浏览器的连接保持更长时间的打开。
Note
如果您看到一个详细的查询说明,显示扫描的记录(nscanned)比它返回的记录(n)多得多,那么该查询可能是索引的候选。
下一步是确定在Tags字段上添加索引是否会提高查询的性能:
> db.posts.ensureIndex({Tags:1})
现在再次运行explain()函数,查看添加索引的效果:
> db.posts.find({Tags:"even"}).explain()
{
"cursor" : "BtreeCursor Tags_1",
"isMultiKey" : true,
"n" : 14997,
"nscannedObjects" : 14997,
"nscanned" : 14997,
"nscannedObjectsAllPlans" : 14997,
"nscannedAllPlans" : 14997,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 4,
"indexBounds" : {
"Tags" : [
[
"even",
"even"
]
]
},
"server" : "Pixl.local:27017"
}
查询的性能有了显著的提高。您可以看到该查询现在使用了由Tags_1索引驱动的BtreeCursor。扫描的记录数量从 29,997 条减少到了您期望查询返回的 14,997 条,执行时间也减少到了 4 毫秒。
Note
最常见的索引类型,也是 MongoDB 唯一使用的索引类型,是 btree(二叉树)。BtreeCursor是一个 MongoDB 数据游标,它使用二叉树索引在文档间导航。Btree 索引在数据库系统中非常常见,因为它们提供了快速的插入和删除,而且在用于遍历或排序数据时还提供了合理的性能。
管理索引
您现在已经看到了引入精心选择的索引会产生多大的影响。
正如您在第 3 章中了解到的,MongoDB 的索引既用于查询(find、findOne)也用于排序。如果您打算在集合中使用很多排序,那么您应该添加符合您的排序规范的索引。如果在排序规范中没有字段索引的集合上使用sort(),那么如果超出了内部排序缓冲区的最大大小,可能会得到一条错误消息。因此,为排序创建索引是一个好主意。在接下来的几节中,我们将再次触及基础知识,但也会添加一些与如何管理和操作系统中的索引相关的细节。我们还将介绍这些索引与一些样本的关系。
当您向集合添加索引时,MongoDB 必须维护它,并在您每次执行任何写操作时更新它(例如,updates、inserts或deletes)。如果集合中有太多索引,可能会对写入性能产生负面影响。
索引最适合用于大多数访问是读访问的集合。对于日志记录系统中使用的大量写操作的集合,引入索引会降低每秒钟流入集合的峰值文档数。
Warning
此时,每个集合最多可以有 64 个索引。
列出索引
MongoDB 有一个简单的助手函数getIndexes(),用于列出给定集合的索引。执行时,它将打印一个 JSON 数组,该数组包含给定集合上每个索引的详细信息,包括它们引用的字段或元素以及您可能在该索引上设置的任何选项。
$mongo
>use blog
>db.posts.getIndexes()
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"ns" : "blog.posts",
"name" : "_id_"
}
]
MongoDB 在每个数据库中维护一个名为system.indexes的特殊集合。该集合跟踪数据库中所有集合上创建的所有索引。
system.indexes收藏就像任何普通的收藏一样。您可以列出其内容,对其运行查询,或者执行您可以使用典型集合完成的常规任务。
以下示例列出了简单数据库中的索引:
$mongo
>use blog
>db.system.indexes.find()
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "blog.posts", "name" : "_id_" }
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "blog.authors", "name" : "_id_" }
blog数据库没有任何用户定义的索引,但是您可以看到为您的两个集合上的_id字段自动创建的两个索引:posts和authors。您不必做任何事情来创建或删除这些身份索引;每当创建或删除一个集合时,MongoDB 都会创建和删除它们。
当您在元素上定义索引时,MongoDB 将构造一个内部 btree 索引,它将使用该索引来高效地定位文档。如果找不到合适的索引,MongoDB 将扫描集合中的所有文档,以找到满足查询的记录。
创建简单索引
MongoDB 提供了用于向集合添加新索引的ensureIndex()函数。这个函数首先检查是否已经用相同的规范创建了一个索引。如果有,那么ensureIndex()就返回那个索引。这意味着您可以任意多次调用ensureIndex(),但这不会导致为您的集合创建大量额外的索引。
以下示例定义了一个简单的索引:
$mongo
>use blog
>db.posts.ensureIndex({Tags:1})
这个例子在Tags字段上创建了一个简单的升序 btree 索引。相反,创建一个降序索引只需要一个小小的改动:
>db.posts.ensureIndex({Tags:-1})
要索引嵌入文档中的字段,可以使用普通的点标记寻址方案;也就是说,如果您有一个位于comments子文档中的count字段,您可以使用以下语法对其进行索引:
>db.posts.ensureIndex({"comments.count":1})
如果您指定一个数组类型的文档字段,那么索引将包含数组的所有元素作为单独的索引项。这称为多键索引,每个文档都链接到索引中的多个值。如果您回过头来检查我们之前查询的explain()输出,您可以看到这里提到了这一点。
MongoDB 有一个特殊的操作符all,用于执行希望只选择包含您提供的所有术语的文档的查询。在blog数据库示例中,您有一个包含元素Tags的posts集合。这个元素包含了与文章相关的所有标签。以下查询查找同时具有sailor和moon标签的所有文章:
>db.posts.find({Tags:{$all: ['sailor', 'moon']}})
如果在Tags字段上没有多键索引,查询引擎将不得不扫描集合中的每个文档,以查看其中一个术语是否存在,如果存在,则检查两个术语是否都存在。
创建复合索引
简单地为任何查询中提到的每个字段创建一个单独的索引可能很有诱惑力。虽然这可以在不需要太多考虑的情况下加快查询速度,但不幸的是,这会对添加和删除数据库中的数据产生重大影响,因为这些索引每次都需要更新。同样重要的是要注意,在 MongoDB 2.4 和更早的版本中,只有一个索引会被用来满足查询的结果,所以添加许多小索引通常不会帮助查询的执行。
复合索引提供了一种减少集合中索引数量的好方法,允许您将多个字段组合成一个索引,因此您应该尽可能使用复合索引。
复合索引主要有两种类型:子文档索引和手动定义的复合索引。
MongoDB 有一些规则允许它对不使用所有组件键的查询使用复合索引。理解这些规则使您能够构建一组复合索引,覆盖您希望对集合执行的所有查询,而不必单独索引每个元素(从而避免前面提到的对插入/更新性能的附带影响)。
复合索引可能没有用的一个方面是在排序中使用索引。排序不擅长使用复合索引,除非术语列表和排序方向与索引结构完全匹配。
当您使用子文档作为索引键时,用于建立多键索引的元素的顺序与它们在子文档的内部 BSON 表示中出现的顺序相匹配。在许多情况下,这并不能让您对创建索引的过程有足够的控制。
要绕过这一限制,同时保证查询使用以所需方式构造的索引,需要确保使用相同的子文档结构来创建形成查询时使用的索引,如下例所示:
>db.articles.find({author:{name: 'joe', email: 'joe@blogger.com '}))
还可以通过命名索引中要组合的所有字段,然后指定组合它们的顺序,来显式创建复合索引。以下示例说明了如何手动构建复合索引:
>db.posts.ensureIndex({"author.name":1, "author.email":1})
指定索引选项
创建索引时可以指定几个有趣的选项,比如创建唯一索引或启用后台索引;在接下来的章节中,您将了解更多关于这些选项的信息。您将这些选项指定为ensureIndex()函数的附加参数,如下例所示:
>db.posts.ensureIndex({author:1}, {option1:true, option2:true, ..... })
使用{background:true}在后台创建索引
当您第一次使用ensureIndex()函数指示 MongoDB 创建索引时,服务器必须读取集合中的所有数据并创建指定的索引。默认情况下,索引构建在前台完成,在索引操作完成之前,对集合数据库的所有操作都被阻止。
MongoDB 还包含一个特性,允许在后台执行索引的初始构建。在建立索引时,其他连接对该数据库的操作不会被阻止。在索引建立之前,任何查询都不会使用它,但是服务器将允许读写操作继续进行。一旦索引操作完成,所有需要该索引的查询将立即开始使用它。值得注意的是,虽然可以在后台构建索引,但它需要更长的时间才能完成。
因此,您可能希望研究构建索引的其他策略。最好的策略可能是在一个副本集中轮流执行构建。为此,您一次停止一个辅助节点,并在没有其--replSet参数的情况下在不同的端口上启动它,暂时使它成为一个独立的节点。然后,您可以放心地在这个辅助服务器上执行索引构建。索引构建完成后,像往常一样启动辅助服务器,让它重新加入副本集并跟上。对所有次要成员重复此过程。最后,停止主服务器,对--replSet执行相同的删除,并为它构建一个索引进程,然后像往常一样让它重新加入副本集。通过以这种方式旋转您的副本集,您可以在没有停机或中断的情况下建立索引!
Note
该索引将在后台构建。但是,如果从 MongoDB shell 发出命令,发起请求的连接将被阻塞。在索引操作完成之前,在此连接上发出的命令不会返回。乍一看,这似乎与它是一个背景索引的想法相矛盾。但是,如果您同时打开了另一个 MongoDB shell,您会发现在索引构建过程中,对该集合的查询和更新会不受阻碍地运行。只有发起连接被阻塞。这不同于您看到的简单的ensureIndex()命令的行为,该命令不在后台运行,因此第二个 MongoDB shell 上的操作也会被阻塞。
KILLING THE INDEXING PROCESS
如果您认为当前的索引进程已经挂起或者花费了太长时间,您也可以终止它。您可以通过调用killOp()函数来实现:
> db.killOp(<operation id>)
要运行 killOp,您需要知道操作的操作 ID。通过运行db.currentOp()命令,可以获得 MongoDB 实例上当前运行的所有操作的列表。
注意,当您调用killOp()命令时,部分索引也将再次被删除。这可以防止数据库中出现不完整或不相关的数据。
创建具有唯一键{unique:true}的索引
当您指定unique选项时,MongoDB 会创建一个索引,其中所有的键都必须不同。这意味着,如果您试图插入一个索引键与现有文档的键相匹配的文档,MongoDB 将返回一个错误。这对于您希望确保没有两个人拥有相同身份(即相同的userid)的领域非常有用。
但是,如果要向已经填充了数据的现有集合添加唯一索引,则必须确保已经对键进行了重复数据删除。在这种情况下,如果任意两个键不唯一,创建索引的尝试将会失败。
unique选项适用于简单和复合索引,但不适用于多键值索引,在多键值索引中它们没有多大意义。
如果插入的文档缺少一个被指定为惟一键的字段,那么 MongoDB 将自动插入该字段,但是会将其值设置为null。这意味着您只能将一个缺少关键字段的文档插入到这样的集合中;任何额外的null值都意味着这个键不是惟一的,正如所要求的那样。
使用{dropdups:true}自动删除重复项
如果您想为已知存在重复键的字段创建唯一索引,您可以指定dropdups选项。该选项指示 MongoDB 删除会导致索引创建失败的文档。在这种情况下,MongoDB 将保留它在集合的自然排序中找到的第一个文档,但随后丢弃任何其他会导致违反索引约束的文档。
Warning
使用dropdups选项时需要非常小心,因为它会导致文档从您的收藏中删除。
在使用此选项之前,您应该非常清楚您的集合中的数据;否则,您可能会得到意想不到的(更不用说不想要的)行为。对希望使键唯一的集合运行组查询是一个非常好的主意;这将使您能够在执行此选项之前确定被视为重复的文档数量。
使用{sparse:true}创建稀疏索引
有时,只为包含给定字段条目的文档创建索引是值得的。例如,假设您想要索引电子邮件,并且您知道并非所有电子邮件都有“抄送”或“密件抄送”字段。如果您在 CC 或 BCC 上创建索引,那么所有文档都将添加一个“null”值,除非您指定一个稀疏索引。这是一种节省空间的机制,因为您只对有效文档而不是所有文档进行索引。当然,这对于任何运行和使用稀疏索引的查询都有影响;因为可能存在查询中未评估的文档。
TTL 索引
在计算术语中,TTL(生存时间)是一种通过指定一个点来赋予特定数据或请求一个生命周期的方式,在该点上它变得无效。这对于存储在 MongoDB 实例中的数据也很有用,因为自动删除旧数据通常很好。为了创建一个 TTL 索引,你必须添加一个expireAfterSeconds标志和一个秒值到一个单一的(非复合的)索引。这将表明,当 TTL 删除任务下次执行时,任何索引字段大于给定 TTL 值的文档都将被删除。由于删除任务每 60 秒运行一次,因此在删除旧文档之前可能会有一些延迟。
Warning
被索引的字段必须是 BSON 日期类型;否则不会被评估为删除。如下例所示,当从 shell 中查询时,BSON 日期将显示为ISODate。
例如,假设我们想从博客的comments集合中自动删除任何内容,该集合的创建时间戳超过了某个年龄。以这个来自comments集合的示例文档为例:
>db.comments.find();
{
"_id" : ObjectId("519859b32fee8059a95eeace"),
"author" : "david",
"body" : "foo",
"ts" : ISODate("2013-05-19T04:48:51.540Z"),
"tags" : [ ]
}
假设我们想要删除任何超过 28 天的评论。我们算出 28 天有 2419200 秒长。然后,我们按如下方式创建索引:
>db.comments.ensureIndex({ts:1},{ expireAfterSeconds: 2419200})
当此文档比当前系统时间早 2,419,200 秒时,它将被删除。您可以通过使用以下语法创建一个超过 2,419,200 秒的文档来进行测试:
date = new Date(new Date().getTime()-2419200000);
db.comments.insert({ "author" : test", "body" : "foo", "ts" : date, "tags" : [] });
现在只需等待一分钟,该文件应被删除。
Note
当您设置 TTL 索引时,MongoDB 会在给定的集合上设置usePowerOf2Sizes标志。这将使每个文件的存储空间标准化。这允许空间在被删除后被未来的文档更有效地重用。
文本搜索索引
MongoDB 2.4 引入了一种新的索引类型——文本索引,它允许您执行全文搜索!第 8 章详细讨论了文本索引特性,但在我们研究优化时,这里有必要快速总结一下。文本搜索一直是 MongoDB 的一个理想特性,因为它允许您在一个大的文本块中搜索特定的单词或文本。文本搜索相关性的最好例子是对博客文章正文的搜索功能。这种搜索允许您在文档的一个文本字段(如正文)或多个文本字段(如正文和注释)中查找单词或短语。因此,为了创建文本索引,我们运行以下命令:
>db.posts.ensureIndex( { body: "text" } )
Note
文本搜索不区分大小写,这意味着它将忽略大小写;“mongodb”和“MongoDB”被视为同一文本。
现在我们可以使用文本索引和text命令进行搜索。为此,我们使用runCommand语法来使用text命令,并为其提供一个要搜索的值:
>db.posts.runCommand( "text", { search: "MongoDB" } )
您的文本搜索结果将按相关性顺序返回给您。
Warning
MongoDB 文本搜索仍然相对较新,并且正在经历快速发展。
删除索引
您可以选择删除集合中的所有索引或仅删除一个特定索引。使用以下函数删除集合中的所有索引:
>db.posts.dropIndexes()
要从集合中删除单个索引,可以使用与使用ensureIndex()创建索引的语法相对应的语法:
>db.posts.dropIndex({"author.name":1, "author.email":1});
重新索引收藏
如果您怀疑集合中的索引已损坏,例如,如果您的查询得到不一致的结果,那么您可以强制对受影响的集合重新建立索引。
这将强制 MongoDB 删除并重新创建指定集合上的所有索引(有关如何检测和解决索引问题的更多信息,请参见第 9 章),如下例所示:
> db.posts.reIndex()
{
"nIndexesWas" : 2,
"msg" : "indexes dropped for collection",
"nIndexes" : 2,
"indexes" : [
{
"key" : {
"_id" : 1
},
"ns" : "blog.posts",
"name" : "_id_"
},
{
"key" : {
"Tags" : 1
},
"ns" : "blog.posts",
"name" : "Tags_1"
}
],
"ok" : 1
}
输出列出了该命令重新构建的所有索引,包括键。此外,nIndexWas:字段显示了在运行命令之前存在多少个索引,而nIndex:字段给出了命令完成之后的索引总数。如果两个值不相同,则意味着在重新创建集合中的某些索引时出现了问题。
MongoDB 如何选择它将使用的索引
当一个数据库系统需要运行一个查询时,它必须组装一个查询计划,查询计划是它执行查询所必须运行的步骤的列表。每个查询可能有多个查询计划,这些计划同样可以产生相同的结果。然而,每个计划都可能包含比其他计划执行起来更昂贵的元素。例如,扫描集合中的所有记录是一项开销很大的操作,任何包含这种方法的计划都会很慢。这些计划还可以包括用于查询和排序操作的备选索引列表。
道路指示很好地说明了这一概念。如果你想从一个拐角到达街区的斜对面,那么“左转,然后右转”和“右转,然后左转”是到达对面拐角的同样有效的计划。然而,如果其中一条路线有两个停车标志,而另一条没有,那么前一种方法是一个更昂贵的方案,而后一种方法是最好的方案。在执行查询时,集合扫描可能会成为潜在的停止标志。
MongoDB 附带了一个名为查询分析器的组件。这个组件接受一个查询和查询目标的集合,然后为 MongoDB 生成一组要执行的计划。本章前面描述的explain()函数列出了所使用的计划和为给定查询生成的一组备选计划。
MongoDB 还附带了一个查询优化器组件。该组件的工作是选择最适合运行特定查询的执行计划。在大多数关系数据库系统中,查询优化器使用关于表中键的分布、记录数量、可用索引、先前选择的有效性以及各种加权因子的统计信息来计算每种方法的成本。然后,它选择最便宜的计划来使用。
MongoDB 中的查询优化器比典型的 RDBMS 查询分析器既笨又聪明。例如,它不使用基于成本的方法来选择执行计划;相反,它并行运行所有这些程序,并使用返回结果最快的程序,在获胜者越过终点线后终止所有其他程序。因此,MongoDB 中的查询分析器使用一种简单的机制(dumb)来确保它总是获得最快的结果(smart ),并且这些计划的结果被缓存并在接下来的 1000 次写操作中重用,直到执行解释或在该集合上添加、删除或重新索引任何索引。
使用 hint()强制使用特定索引
MongoDB 中的查询优化器从查询的候选索引集中选择最佳索引。它使用刚才概述的方法来尝试将最佳索引或索引集与给定的查询进行匹配。但是,在有些情况下,查询优化器可能没有做出正确的选择,在这种情况下,可能有必要帮助组件。
在这种情况下,您可以向查询优化器提供提示,促使组件做出不同的选择。例如,如果您已经使用了explain()来显示您的查询正在使用哪些索引,并且您认为您希望它对给定的查询使用不同的索引,那么您可以强制查询优化器这样做。
让我们看一个例子。假设您有一个名为author的子文档的索引,其中有name和email字段。还假设您有以下定义的索引:
>db.posts.ensureIndex({author.name:1, author.email:1})
您可以使用以下提示来强制查询优化器使用定义的索引:
>db.posts.find({author:{name:'joe', email: 'joe@mongodb.com '}}).hint({author.name:1, author.email:1})
如果出于某种原因,您希望强制查询不使用索引,也就是说,如果您希望使用集合文档扫描作为选择记录的方法,您可以使用以下提示来完成此操作:
>db.posts.find({author:{name: 'joe', email: 'joe@mongodb.com '}}).hint({$natural:1})
优化小对象的存储
索引是加速数据查询的关键。但是影响应用性能的另一个因素是它所访问的数据的大小。与具有固定模式的数据库系统不同,MongoDB 将每个记录的所有模式数据存储在记录本身中。因此,对于每个字段具有大量数据内容的大型记录,模式数据与记录数据的比率较低;但是,对于具有小数据值的小记录,这个比率可能会变得惊人地大。
考虑一个 MongoDB 非常适合的应用类型中的常见问题:日志记录。MongoDB 非凡的写入速度使得将事件作为小文档流式传输到集合中非常高效。但是,如果您想进一步优化执行该功能的速度,您可以做几件事情。
首先,您可以考虑批量插入。MongoDB 附带了一个多文档insert()调用。您可以使用此调用同时将几个事件放入集合中。这导致通过数据库接口 API 的往返次数减少。
其次(也是更重要的),您可以减少字段名的大小。如果字段名较小,MongoDB 可以在将事件记录刷新到磁盘之前将更多的事件记录打包到内存中。这使得整个系统效率更高。
例如,假设您有一个用于记录三个字段的集合:一个时间戳、一个计数器和一个用于指示数据源的四字符字符串。您的数据的总存储大小如表10–2所示。
表 10–2。
The Logging Example Collection Storage Size
| 田 | 大小 | | --- | --- | | 时间戳 | 8 字节 | | 整数 | 4 字节 | | 线 | 4 字节 | | 总数 | 16 字节 |如果您使用ts、n和src作为字段名,那么字段名的总大小是 6 个字节。与数据大小相比,这是一个相对较小的值。但是现在假设您决定将字段命名为WhenTheEventHappened、NumberOfEvents和SourceOfEvents。在这种情况下,字段名的总大小是 48 个字节,是数据本身大小的三倍。如果您将 1TB 的数据写入一个集合,那么您将存储 750GB 的字段名称,但只有 250GB 的实际数据。
这不仅浪费了磁盘空间。它还会影响系统性能的所有其他方面,包括索引大小、数据传输时间,以及(可能更重要的是)使用宝贵的系统 RAM 来缓存数据文件。
在日志应用中,您还需要避免在写入记录时在集合上添加索引;如前所述,维护索引需要时间和资源。相反,您应该在开始分析数据之前立即添加索引。
最后,您应该考虑使用将事件流分成多个集合的模式。例如,您可以将每天的事件写入单独的集合。较小的集合需要较少的时间进行索引和分析。
摘要
在这一章中,我们研究了一些跟踪 MongoDB 查询中缓慢性能的工具,以及加速由此产生的缓慢查询的潜在解决方案。我们还研究了一些优化数据存储的方法。例如,我们研究了确保充分利用 MongoDB 服务器可用资源的方法。
本章中描述的特定技术使您能够优化数据并调优存储数据的 MongoDB 系统。最佳方法因应用而异,取决于许多因素,包括应用类型、数据访问模式、读/写比率等。
十一、复制
Abstract
像它的许多关系表兄弟一样,MongoDB 支持实时或接近实时地将数据库的内容复制到另一个服务器。MongoDB 的复制特性设置和使用都很简单。它们也是 MongoDB 中的关键特性之一,与分片一起,支持数据库既是 Web 2.0 又是基于云的数据存储的说法。
像它的许多关系表兄弟一样,MongoDB 支持实时或接近实时地将数据库的内容复制到另一个服务器。MongoDB 的复制特性设置和使用都很简单。它们也是 MongoDB 中的关键特性之一,与分片一起,支持数据库既是 Web 2.0 又是基于云的数据存储的说法。
在很多情况下,您可能需要使用复制,因此 MongoDB 中的复制支持必须足够灵活,以便能够应对所有这些情况。MongoDB,Inc .的 MongoDB 架构师竭尽全力确保其复制实现满足当今的所有需求。
在本章中,我们将介绍 MongoDB 中复制的基础知识,包括以下主题:
- MongoDB 中的复制是什么?
- 什么是初选?
- 什么是次要的?
- 什么是操作日志?
Note
复制是 MongoDB 中继续发展的一个特性,随着产品的发展,您可以预期复制的工作方式会发生一些变化。对于数据库服务器的集群来说尤其如此。这本书的第一版和第二版之间已经有了一些变化。MongoDB,Inc .投入了相当大的努力来确保 MongoDB 满足并超越每个人对可伸缩性和可用性的期望;复制支持是 MongoDB 的关键特性之一。Inc 正指望帮助它满足这些期望。
在详细查看复制设置之前,让我们回顾一下各种设置要实现的目标。我们还将概述复制目前在 MongoDB 中如何工作的一些基础知识,并查看操作日志及其在副本集成员之间的数据复制中的作用。这些主题构成了理解复制的基础。
阐明 MongoDB 的复制目标
其中,复制可用于实现可伸缩性、持久性/可靠性和隔离。在接下来的部分中,我们将探索如何使用复制来实现这些目标,同时指出要避免的潜在陷阱和错误。
提高可扩展性
特别是对于 web 应用,可伸缩性是一个关键的设计要求,尤其是那些严重依赖后端数据库的应用。复制可以通过两种方式帮助您创建更具伸缩性的应用:
- 提高冗余度:复制可以让您在多个数据中心托管一个应用,从而帮助您提高冗余度。在这种方法中,您确保每个数据中心都有数据的本地副本,以便应用可以高速访问它。然后,用户可以连接到离他们最近的数据中心,从而最大限度地减少延迟。
- 提高性能:在某些情况下,复制可以帮助您提高应用的原始性能。当您有一个大型 web 应用,其中的数据集主要是基于读取的,并且您希望将查询分布到多个数据库服务器以提高并行性时,这种情况尤其如此。或者具有非常不同的工作集的查询负载,例如报告或聚合。
Note
MongoDB 还支持一个称为分片的特性,该特性旨在帮助您创建更具可伸缩性的应用,无论有无复制都可以实现真正的高可伸缩性。参见第 12 章了解更多关于在 MongoDB 中一起使用分片和复制的信息。
提高耐用性/可靠性
复制通常用于帮助防范硬件故障或数据库损坏,并在执行备份或其他潜在高影响维护活动时提供灵活性,影响很小或没有影响,因为这些任务可以在集合成员上单独执行,而不会影响整个集合。人们以这种方式使用复制的一些具体示例包括:
- 当您希望拥有延迟运行的数据库的副本时。您可能希望保护自己免受应用缺陷的影响,或者提供一种简单的机制,通过突出显示两个数据集的查询结果之间的差异来提供趋势信息。这还可以为人为错误提供安全缓冲,并避免从备份中完全恢复的需要。
- 当你需要一个备份系统以防失败时。如果在系统出现故障时,正常的备份方案需要很长时间才能恢复,您可能需要运行副本作为备份。
- 当您出于管理目的需要冗余系统时。您可能希望运行一个复制副本,以便可以在节点之间轮换执行管理任务,如备份或升级。
提供隔离
如果对生产数据库运行某些进程,将会显著影响该数据库的性能或可用性。您可以使用复制来创建将流程与生产数据库隔离的同步副本,例如:
- 当您希望在不影响生产系统性能的情况下运行报告或备份时:维护隐藏的辅助副本使您能够将查询与报告系统隔离开来,并确保月末报告不会延迟或影响您的正常操作。
复制基础
正如您所看到的,副本集(或 replSet)是一种设置多个 MongoDB 实例的方法,以包含相同的数据用于冗余和其他相关措施。除了了解这些,您还应该了解 MongoDB 是如何完成其复制的,这样您就知道如何最好地管理您自己的副本集。
您已经知道了 MongoDB 中复制的目标,如果您已经阅读了这本书的第一版或者从早期就开始使用 MongoDB,您将会知道有许多不同的方法可以完成复制,包括:
- 主/从复制
- 主/主复制
- 复制对
这些复制方法都已被副本集的概念所取代。在 MongoDB 中,一个副本集由一个主节点和一些辅助或仲裁节点组成。副本集应该由奇数个成员组成,即最少三个。出现这一需求是因为 MongoDB 副本集有一个规则,即主节点必须能够看到大多数其他节点,以便允许它继续作为主节点。实施这一规则是为了避免“裂脑”的情况,即由于网络中的潜在故障,你有两个主要的,如图 11-1 所示。

图 11-1。
The Split-brain problem
什么是初选?
就副本集而言,主要副本是副本集在给定时刻的真实来源。它是集合中唯一可以写入数据的节点,所有其他节点都可以从该节点复制数据。初选由所有投票成员的多数票选举产生,即法定人数。
一旦选择了主节点,所有辅助节点将使用它作为其复制的真实源,因此所有写入都必须定向到该成员。
什么是次要的?
次要成员是携带数据的非主要成员,理论上可以(除了少数例外)成为主要成员。它是一个节点,可以以尽可能接近实时的方式从其数据集中的主节点读取和复制数据。默认情况下,如果在没有任何读取首选项的情况下直接连接到辅助节点,则无法执行读取操作。这样做是为了强调,对于任何对非主数据库的读取,如果复制有延迟,您可能会从较旧的数据中读取。您可以使用命令rs.slaveOk()来设置当前连接,以便从辅助节点读取数据。或者,如果你正在使用一个驱动程序,你可以设置一个读优先选项,我们将在本章后面讨论。
Note
初选的概念是短暂的,也应该是短暂的。也就是说,理想情况下,您应该没有哪个节点是主节点的“固定”概念。在副本集中,所有辅助节点都在写入与主节点相同的数据,以便跟上复制。因此,如果辅助节点的能力大大降低,它们可能无法应对升级为主节点的情况。
什么是仲裁人?
仲裁器是一个非数据承载节点,用于提供额外的投票,以帮助维持副本集选举的多数。它不投决定性的一票,也不指示哪个节点是主要节点,但是参与并可以成为决定主要节点的法定成员。仲裁器最好用于帮助避免前面描述的“裂脑”问题。考虑图 11-2 所示的图表。通过给 A 站点添加一个仲裁者,我们总是可以有一方可以创建多数。这意味着在网络中断的情况下,我们不会有两个初选!我们可以通过在第三个站点 c 中设置仲裁器来进一步增加冗余。这样,如果站点 A 发生故障,我们仍然可以从站点 B 和 c 中的节点中形成多数。使用这样的第三个站点,我们可以在失去与任何一个站点的连接时继续运行。

图 11-2。
Split Brain Problem Solved
深入操作日志
简而言之,操作日志(operation log)是一个有上限的集合,其中包含主实例对其数据库所做更改的滚动记录,目的是向辅助实例重放这些更改,以确保数据库是相同的。副本集的每个成员维护其自己的操作日志,并且辅助节点向主节点(或其他更新的辅助节点)的操作日志查询新条目以应用于它们自己的所有数据库副本。
操作日志为每个条目创建一个时间戳。这使得辅助节点能够跟踪它在前一次读取期间从操作日志读取了多远,以及它需要传输哪些条目来赶上。如果您停止辅助节点并在相对较短的时间后重新启动它,它将使用主节点的操作日志来检索它在离线时错过的所有更改。
因为拥有无限大的操作日志是不实际的,所以操作日志被限制或限定在特定的大小。
您可以将操作日志视为主实例最近活动的窗口;如果该窗口太小,操作将在应用到辅助节点之前从操作日志中丢失。如果尚未在当前实例上创建操作日志,默认情况下- oplogSize启动选项允许您以 MB 为单位设置操作日志的大小。对于 Linux 或 Windows 64 位系统,oplogSize将被设置为可用于数据存储的可用磁盘空间的 5%。如果您的系统是写/更新密集型的,那么您可能需要增加这个大小,以确保从机可以脱机一段合理的时间而不丢失数据。
例如,如果您有一个需要一个小时才能完成的从机每日备份,则必须设置操作日志的大小,以允许从机在该小时内保持离线,再加上额外的时间量,以提供安全余量。
在为操作日志计算合适的大小时,考虑主服务器上所有数据库的更新率是非常重要的。
您可以通过使用 db 来了解适合您的操作日志的大小。在主实例上运行的printReplicationInfo()命令:
$mongo
>db.printReplicationInfo()
configured oplog size: 15000MB
log length start to end: 6456672secs (1793.52hrs)
oplog first event time: Wed Mar 20 2013 17:00:43 GMT+1100 (EST)
oplog last event time: Mon Jun 03 2013 09:31:55 GMT+1000 (EST)
now: Mon Jun 03 2013 20:22:20 GMT+1000 (EST)
此命令显示您的操作日志的当前大小,以及以当前更新速率填满所需的时间。根据这些信息,您可以估计是否需要增加或减少操作日志的大小。您还可以通过查看 MongoDB Monitoring Service (MMS)中的repl lag部分来查看副本集的给定成员离主成员有多远。如果您还没有安装 MMS,我真的建议您现在就安装,因为您的 MongoDB 集群变得越来越大,MMS 提供的统计信息就变得越来越重要。要了解更多背景知识,您应该查看第 9 章的彩信部分。
实现副本集
在本节中,您将学习如何设置一个简单的副本集配置。您还将学习如何在集群中添加和删除成员。如前所述,副本集基于单个主服务器和多个辅助或仲裁服务器的概念,这些辅助或仲裁服务器将从主服务器复制写入(见图 11-3 )。

图 11-3。
A cluster implemented with a replica set
副本集还具有主动和被动成员的概念。当当前主服务器不可用时,被动辅助服务器不参与新主服务器的选举;相反,它们的作用与隐藏成员相同,可以用作报告或备份数据集。
启动时,副本集的成员服务器不需要被指定为集成员。相反,配置是通过普通服务器界面发送的服务器级命令来完成的。这使得创建允许动态配置和管理机器集群的配置管理工具变得更加容易。
在接下来的部分中,您将学习如何完成以下任务:
Create a replica set. Add a server to a replica set. Add an arbiter to a replica set. Inspect and perform operations on a replica set. Configure individual members of a replica set. Connect to a replica set from your application. Set Read Preference from within your application Set Write Concern from within your application Use Replica Set tags with Read Preference and Write Concern Use the web interface to inspect the status of your replica set
创建副本集
学习如何创建副本集的最好方法是看一个例子。在下面的例子中,您将创建一个名为testset的副本集。该集合将有三个成员(两个主动成员和一个被动成员)。表 11-1 列出了这个集合的成员。
表 11-1。
Configuring the Replica Set
| 服务 | 守护进程 | 地址 | 数据库路径 | | --- | --- | --- | --- | | 活动成员 1 | `mongod` | `[hostname]:27021` | `/db/active1/data` | | 活动成员 2 | `mongod` | `[hostname]:27022` | `/db/active2/data` | | 被动构件 1 | `mongod` | `[hostname]:27023` | `/db/passive1/data` |副本集将允许您使用 localhost 作为标识符,但仅当所有机器都位于一台服务器上时。发生这种情况是因为副本集的每个成员必须能够通过主机名联系所有其他 MongoDB 实例,复制才能正常工作。
通常当使用副本集时,我们使用主机名;您可以使用the hostname命令找到当前主机名,如下例所示:
$hostname
Pixl.local
在接下来的例子中,通过在您自己的系统上运行hostname命令,将术语[hostname]替换为返回的任何值。
启动并运行副本集成员
第一步是让第一个活动成员启动并运行。为此,请打开终端窗口并键入以下内容:
$ mkdir -p /db/active1/data
$ mongod --dbpath /db/active1/data --port 27021 --replSet testset
--replSet选项告诉实例它正在加入的副本集的名称。这是副本集的第一个成员,因此您可以给它任何其他成员的地址,即使该成员尚未启动。只需要一个成员地址,但您也可以提供其他成员的名称,方法是用逗号分隔他们的地址,如下例所示:
$ mongod --dbpath /db/active1/data --port 27021 --replSet testset
Note
如果您不希望在自己的 shell 实例中运行这些 MongoDB 实例,您可以添加–-fork和–-logpath < file >选项,告诉这个实例在后台打开自己,并将其日志记录到指定的文件。
为了简单起见,这个例子只依赖一个地址。下一步是让其他成员开始工作。为此,请再打开两个终端窗口,然后在第一个窗口中键入以下内容,以启动并运行第二个成员:
$ mkdir -p /db/active2/data
$ mongod --dbpath /db/active2/data --port 27022 --replSet testset
接下来,在第二个窗口中键入以下内容,启动并运行最后一个(被动)成员:
$ mkdir -p /db/passive1/data
$ mongod --dbpath /db/passive1/data --port 27023 --replSet testset
此时,您有三个服务器实例正在运行并相互通信;但是,您还没有运行您的副本集,因为您还没有初始化副本集,也没有告诉每个成员它的角色和职责。
为此,您需要连接到其中一个服务器并初始化副本集。以下代码选择要连接的第一台服务器:
$mongo [``hostname
接下来,您需要初始化该集合的第一个成员,以创建它的操作日志和默认配置文档。您可以在日志文件中看到 MongoD 实例建议您需要这样做:
Mon Jun 3 21:25:23.712 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Mon Jun 3 21:25:23.712 [rsStart] replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done
所以运行rs.initiate命令:
> rs.initiate()
{
"info2" : "no configuration explicitly specified -- making one",
"me" : "[hostname]:27021",
"info" : "Config now saved locally. Should come online in about a minute.",
"ok" : 1
}
最后,您应该检查副本集的状态,以确定其设置是否正确:
>rs.status()
{
"set" : "testset",
"date" : ISODate("2013-06-03T11:28:58Z"),
"myState" : 1,
"members" : [
{
"_id" : 0,
"name" : "[hostname]:27021",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 264,
"optime" : {
"t" : 1370258919,
"i" : 1
},
"optimeDate" : ISODate("2013-06-03T11:28:39Z"),
"self" : true
}
],
"ok" : 1
}
这里的输出表明一切正常:您已经成功地配置、设置和初始化了一个新的副本集。请记住,您应该使用您自己的机器名称来代替[hostname],因为“localhost”和“127.0.0.1”都不起作用。
将服务器添加到副本集
现在您已经启动了新的副本集,您需要开始向其中添加成员。让我们从添加第一个辅助节点开始。只需添加rs.add()命令并提供该实例的主机名和端口,就可以做到这一点。要添加,请连接到您的主服务器并运行以下命令:
$ mongo [``hostname
> rs.add("[``hostname
{ "ok" : 1 }
您需要等待一两分钟,因为该节点会使自己联机,创建自己的操作日志,并准备好自己。您可以使用rs.status()监控进度,同时等待该节点作为辅助节点联机:
>use admin
>rs.status() {
"set" : "testset",
"date" : ISODate("2013-06-03T11:36:37Z"),
"myState" : 1,
"members" : [
{
"_id" : 0,
"name" : "[hostname]:27021",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 723,
"optime" : {
"t" : 1370259331,
"i" : 1
},
"optimeDate" : ISODate("2013-06-03T11:35:31Z"),
"self" : true
},
{
"_id" : 1,
"name" : "[hostname]:27022",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 66,
"optime" : {
"t" : 1370259331,
"i" : 1
},
"optimeDate" : ISODate("2013-06-03T11:35:31Z"),
"lastHeartbeat" : ISODate("2013-06-03T11:36:35Z"),
"lastHeartbeatRecv" : ISODate("2013-06-03T11:36:36Z"),
"pingMs" : 0,
"syncingTo" : "[hostname]:27021"
}
],
"ok" : 1
}
现在让我们利用第三个被动成员。首先像往常一样用rs.add()添加成员:
$ mongo [hostname]:27022
> rs.add("[hostname]:27022")
{ "ok" : 1 }
现在我们需要制作配置文件的副本并修改它。运行下面的命令创建一个名为conf的文档,其中包含您当前的副本集配置。
> conf = rs.conf()
{
"_id" : "testset",
"version" : 3,
"members" : [
{
"_id" : 0,
"host" : "[hostname]:27021"
},
{
"_id" : 1,
"host" : "[hostname]:27022"
},
{
"_id" : 2,
"host" : "[hostname]:27023"
}
]
}
现在您的配置文档已经加载,您需要修改它。我们希望将节点设置为隐藏,优先级为 0,因此它不会被选为主要节点。注意,文档有一个members数组,其中包含副本集每个成员的文档。您需要使用数组操作符[]选择您希望访问的成员。因此,要为第三个成员创建一个值hidden : true,我们需要在 2 处更新数组元素(记住,数组从 0 开始)。运行以下命令:
> conf.members[2].hidden = true
true
现在,我们可以使用相同的命令将优先级值设置为 0:
> conf.members[2].priority = 0
0
您可以通过简单地执行放置配置文档的变量的名称来输出这个配置文档:
> conf
{
"_id" : "testset",
"version" : 3,
"members" : [
{
"_id" : 0,
"host" : "[hostname]:27021"
},
{
"_id" : 1,
"host" : "[hostname]:27022"
},
{
"_id" : 2,
"host" : "[hostname]:27023",
"hidden" : true,
"priority" : 0
}
]
}
如您所见,这个成员现在拥有隐藏值集和优先级 0。现在我们需要做的就是更新副本集配置来使用这个文档。我们通过使用新的配置文档作为参数发出rs.reconfig()命令来实现这一点。
> rs.reconfig(conf)
Tue Jun 4 20:01:45.234 DBClientCursor::init call() failed
Tue Jun 4 20:01:45.235 trying reconnect to 127.0.0.1:27021
Tue Jun 4 20:01:45.235 reconnect 127.0.0.1:27021 ok
reconnected to server after rs command (which is normal)
您的整个副本集失去连接,然后重新连接!发生这种情况是因为重新配置。对副本集的任何更改都可能导致副本集重新配置自己并进行新的选举,在大多数情况下,这将使先前的主要步骤回到其角色中。现在,如果我们重新运行rs.conf()命令,您可以看到新的副本集配置正在运行。
> rs.conf()
{
"_id" : "testset",
"version" : 4,
"members" : [
{
"_id" : 0,
"host" : "[hostname]:27021"
},
{
"_id" : 1,
"host" : "[hostname]:27022"
},
{
"_id" : 2,
"host" : "[hostname]:27023",
"priority" : 0,
"hidden" : true
}
]
}
请注意,此副本集配置的版本号现在已经增加。这作为重新配置的一部分自动发生,以确保任何副本集成员没有错误的配置文档。
现在,您应该有一个完全配置好的三成员副本集,其中有一个活动的主副本集和一个隐藏的“被动”副本集
添加仲裁人
添加仲裁器作为副本集的投票成员是一个简单的过程。让我们从培养一个新成员开始。
$ mkdir -p /db/arbiter1/data
$ mongod --dbpath /db/ arbiter1/data --port 27024 --replSet testset –rest
现在您已经创建了一个新成员,只需使用rs.addArb()命令添加新的仲裁器:
>rs.addArb("[hostname]:27024")
{ "ok" : 1 }
如果您现在运行rs.status(),您将在输出中看到您的仲裁器:
{
"_id" : 3,
"name" : "Pixl.local:27024",
"health" : 1,
"state" : 7,
"stateStr" : "ARBITER",
"uptime" : 721,
"lastHeartbeat" : ISODate("2013-06-07T11:21:01Z"),
"lastHeartbeatRecv" : ISODate("2013-06-07T11:21:00Z"),
"pingMs" : 0
}
你可能已经意识到了这里的一个问题;我们现在有四个节点。那是一个偶数,而且偶数是不好的!如果我们继续这样运行,您的 MongoDB 节点将开始记录以下内容:
[rsMgr] replSet total number of votes is even - add arbiter or give one member an extra vote
为了解决这个问题,我们知道我们需要奇数个成员;因此,一个潜在的解决方案是按照日志消息的建议添加另一个仲裁器,但这并不完美,因为我们添加了不必要的额外复杂性。最佳解决方案是阻止现有节点之一投票并被视为仲裁成员。我们可以通过将隐藏二级选举的投票数设置为零来做到这一点。我们这样做的方式与设置隐藏值和优先级值的方式相同。
conf = rs.conf()
conf.members[2].votes = 0
rs.reconfig(conf)
仅此而已。我们现在已经将被动节点设置为真正的被动节点:它永远不会成为主节点;它被客户端视为副本集的一部分;它不能参加选举或被算作多数。为了测试这一点,您可以尝试关闭被动节点,仲裁器和您的其他两个节点将继续在主节点上运行,而不会发生变化;而在以前,主节点可能会退出,理由是它看不到大多数节点。
副本集链接
您已经看到,通常情况下,副本集的成员会尝试从该副本集中的主副本同步数据。但这不是副本集辅助设备可以同步的唯一位置;它们也可以从其他辅助节点同步。通过这种方式,您的辅助服务器可以形成一个“同步链”,其中每个辅助服务器都会同步副本集中其他辅助服务器的最新数据。
管理副本集
MongoDB 提供了许多命令来管理副本集的配置和状态。表 11-2 显示了可用于创建、操作和检查副本集中集群状态的命令。
表 11-2。
Commands for Manipulating and Inspecting Replica Sets
| 命令 | 描述 | | --- | --- | | `rs.help()` | 返回此表中的命令列表。 | | `rs.status()` | 返回有关副本集当前状态的信息。此命令列出每个成员服务器及其状态信息,包括上次联系的时间。此调用可用于对整个集群进行简单的运行状况检查。 | | `rs.initiate()` | 使用默认参数初始化副本集。 | | `rs.initiate(``replSetcfg` | 使用配置描述初始化副本集。 | | `rs.add("``host``:``port` | 使用提供主机名和(可选)特定端口的简单字符串将成员服务器添加到副本集。 | | `rs.add(``membercfg` | 使用配置描述将成员服务器添加到副本集。如果要指定特定属性(例如,新成员服务器的优先级),必须使用此方法。 | | `rs.addArb("host:port")` | 添加一个新的成员服务器作为仲裁服务器。成员不需要已经用`--replSet`选项启动;在任何可到达的机器上运行的任何`mongod`实例都可以执行这个任务。请注意,副本集的所有成员都可以访问该服务器。 | | `rs.stepDown()` | 当您对副本集的主要成员运行此命令时,使主服务器放弃其角色,并强制在群集中选择新的主服务器。请注意,只有活动的辅助服务器可以作为候选服务器成为新的主服务器,并且如果在 60 秒的等待后没有其他候选成员可用,则可以重新选择原始主服务器。 | | `rs.syncFrom("host:port")` | 从给定成员进行辅助同步。可用于形成同步链。 | | `rs.freeze(secs)` | 冻结给定成员,并使其在指定的秒数内没有资格成为主要成员。 | | `rs.remove("host:port")` | 从副本集中删除给定成员。 | | `rs.slaveOk()` | 设置此连接,以便允许从辅助节点读取。 | | `rs.conf()` | 重新显示当前副本集的配置结构。此命令对于获取副本集的配置结构非常有用。该配置结构可被修改,然后再次提供给`rs.initiate()`以改变该结构的配置。这项技术提供了从副本集中删除成员服务器的唯一受支持的方法;目前还没有直接的方法可以做到这一点。 | | `db.isMaster()` | 这个函数不是特定于副本集的;相反,它是一种通用的复制支持功能,允许应用或驱动程序确定特定的连接实例是否是复制拓扑中的主/主要服务器。 |以下章节详细介绍了表 11-2 中列出的一些更常用的命令,提供了关于它们的功能和使用方法的更多细节。
用 rs.status()检查实例的状态
从我们之前向副本集添加成员的经历中,您应该知道,rs.status()可能是您在处理副本集时最常使用的命令。它允许您检查当前连接的实例的状态,包括它在副本集中的角色:
>rs.status()
> rs.status();
{
"set" : "testset",
"date" : ISODate("2013-06-04T10:57:24Z"),
"myState" : 1,
"members" : [
{
"_id" : 0,
"name" : "[hostname]:27021",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 4131,
"optime" : Timestamp(1370340105, 1),
"optimeDate" : ISODate("2013-06-04T10:01:45Z"),
"self" : true
},
{
"_id" : 1,
"name" : "[hostname]:27022",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 3339,
"optime" : Timestamp(1370340105, 1),
"optimeDate" : ISODate("2013-06-04T10:01:45Z"),
"lastHeartbeat" : ISODate("2013-06-04T10:57:23Z"),
"lastHeartbeatRecv" : ISODate("2013-06-04T10:57:23Z"),
"pingMs" : 0,
"syncingTo" : "[hostname]:27021"
},
{
"_id" : 2,
"name" : "[hostname]:27023",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 3339,
"optime" : Timestamp(1370340105, 1),
"optimeDate" : ISODate("2013-06-04T10:01:45Z"),
"lastHeartbeat" : ISODate("2013-06-04T10:57:22Z"),
"lastHeartbeatRecv" : ISODate("2013-06-04T10:57:23Z"),
"pingMs" : 0,
"syncingTo" : "[hostname]:27021"
}
],
"ok" : 1
}
示例中显示的每个字段都有一个含义,如表 11-3 中所述。这些值可用于了解副本集当前成员的状态。
表 11-3。
Values for the rs.status Fields
| 价值 | 描述 | | --- | --- | | `_id` | 作为副本集一部分的该成员的 ID | | `Name` | 成员的主机名 | | `Health` | `replSet`的健康值 | | `State` | 状态的数值 | | `StateStr` | 这个复制集成员状态的字符串表示 | | `Uptime` | 该成员已经存在多久了 | | `optime` | 对此成员应用的最后一次操作的时间,采用时间戳和整数值的格式 | | `optimeDate` | 上次应用操作的日期 | | `lastHeartbeat` | 上次发送心跳的日期 | | `lastHeartbeatRecv` | 收到的最后一个心跳的日期 | | `pingMs` | 运行`rs.status()`的成员和每个远程成员之间的 ping 时间 | | `syncingTo` | 此给定节点要同步匹配的副本集的成员 |在前面的示例中,rs.status()命令是针对主服务器成员运行的。该命令返回的信息显示主服务器正在以1的myState值运行;换句话说,“成员作为主要(主)成员运行。”
强行举行新的选举,让卢比下台( )
您可以使用rs.stepDown()命令强制主服务器停止运行 60 秒;该命令还强制选择新的主服务器。该命令在下列情况下很有用:
- 当您需要使托管主实例的服务器离线时,无论是调查服务器还是实施硬件升级或维护。
- 当您想要对数据结构运行诊断过程时。
- 当您想要模拟主要故障的影响并强制您的集群进行故障转移时,您可以测试您的应用如何响应这样的事件。
以下示例显示了对testset副本集运行rs.stepDown()命令时返回的输出:
> rs.stepDown()
> rs.status()
{
"set" : "testset",
"date" : ISODate("2013-06-04T11:19:01Z"),
"myState" : 2,
"members" : [
{
"_id" : 0,
"name" : "[hostname]:27021",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 5428,
"optime" : Timestamp(1370340105, 1),
"optimeDate" : ISODate("2013-06-04T10:01:45Z"),
"self" : true
},
{
"_id" : 1,
"name" : "[hostname]:27022",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 4636,
"optime" : Timestamp(1370340105, 1),
"optimeDate" : ISODate("2013-06-04T10:01:45Z"),
"lastHeartbeat" : ISODate("2013-06-04T11:19:00Z"),
"lastHeartbeatRecv" : ISODate("2013-06-04T11:19:00Z"),
"pingMs" : 0,
"syncingTo" : "[hostname]:27021"
},
{
"_id" : 2,
"name" : "[hostname]:27023",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 4636,
"optime" : Timestamp(1370340105, 1),
"optimeDate" : ISODate("2013-06-04T10:01:45Z"),
"lastHeartbeat" : ISODate("2013-06-04T11:19:01Z"),
"lastHeartbeatRecv" : ISODate("2013-06-04T11:19:00Z"),
"pingMs" : 0,
"lastHeartbeatMessage" : "db exception in producer: 10278 dbclient error communicating with server: [hostname]:27021",
"syncingTo" : "[hostname]:27021"
}
],
"ok" : 1
}
在本例中,您对主服务器运行了rs.stepDown()命令。rs.status()命令的输出显示副本集的所有成员现在都是次要的。如果您随后运行rs.status(),您应该看到另一个成员已经升级为主要成员(假设有一个成员符合条件)。
确定成员是否为主服务器
db.isMaster()命令并不严格适用于副本集。然而,这个命令非常有用,因为它允许应用测试当前连接是否连接到主服务器:
>db.isMaster()
{
"setName" : "testset",
"ismaster" : true,
"secondary" : false,
"hosts" : [
"[hostname]:27022",
"[hostname]:27021"
],
"primary" : "[hostname]:27022",
"me" : "[hostname]:27022",
"maxBsonObjectSize" : 16777216,
"maxMessageSizeBytes" : 48000000,
"localTime" : ISODate("2013-06-04T11:22:28.771Z"),
"ok" : 1
}
如果您在此时对您的testset副本集集群运行isMaster(),这表明您运行它的服务器不是主/主服务器("ismaster" == false)。如果运行该命令的服务器实例是副本集的成员,该命令还将返回副本集中已知服务器实例的映射,包括该副本集中各个服务器的角色。
为副本集成员配置选项
副本集功能包括许多选项,可用于控制副本集成员的行为。当您运行rs.initiate( replSetcfg )或rs.add( membercfg )选项时,您必须提供一个描述副本集成员特征的配置结构:
{
_id : <setname>,
members: [
{
_id : <ordinal>,
host : <hostname[:port]>,
[ priority: <priority>, ]
[arbiterOnly : true, ]
[ votes : <n>, ]
[ hidden: true, ]
[ tags: { document }, ]
[ slaveDelay: <seconds>, ]
[ buildIndexes: true, ]
}
, ...
],
settings: {
[ chainingAllowed : <boolean>, ]
[ getLastErrorModes: <modes>, ]
[ getLastErrorDefaults: <lasterrdefaults>, ]
}
}
对于rs.initiate(),您应该提供完整的配置结构,如下所示。配置结构本身的最顶层包括三个层次:_id、members和settings。_id是副本集的名称,当您创建副本集成员时,--replSet命令行选项会提供这个名称。members数组由一组描述集合中每个成员的结构组成;这是在将单个服务器添加到集合中时提供给rs.add()命令的成员结构。最后,settings数组包含适用于整个副本集的选项。
成员结构的组织
members结构包含配置副本集的每个成员实例所需的所有条目;您可以在表 11-4 中看到所有这些条目。
表 11-4。
Configuring Member Server Properties
| [计]选项 | 描述 | | --- | --- | | `members.$._id` | (强制)Integer:该元素指定成员结构在`member`数组中的序号位置。此元素的可能值包括大于或等于`0`的整数。该值使您能够处理特定的成员结构,因此您可以执行添加、移除和覆盖操作。 | | `members.$.host` | (必需)String:该元素以`host:port`的形式指定服务器的名称;注意,主机部分不能是`localhost`或`127.0.0.1`。 | | `members.$.priority` | (可选)Float:该元素表示在选举新的主服务器时分配给服务器的权重。如果主服务器变得不可用,则将基于该值升级辅助服务器。任何非零值的辅助服务器都被认为是活动的,有资格成为主服务器。因此,将该值设置为零会强制辅助节点成为被动节点。如果多个辅助服务器共享相同的优先级,那么将进行投票,并且可以调用仲裁器(如果配置的话)来解决任何死锁。该元素的默认值是`1.0`。 | | `members.$.arbiterOnly` | (可选)Boolean:该成员作为仲裁人选举新的主服务器。它不涉及副本集的任何其他功能,并且不需要使用`--replSet`命令行选项启动。系统中任何正在运行的`mongod`进程都可以执行这个任务。这个元素的默认值是`false`。 | | `members.$.votes` | (可选)Integer:此元素指定实例可以投票选举其他实例作为主服务器的票数;这个元素的默认值是`1`。 | | `members.$. hidden` | (可选)Boolean:这将从`db.isMaster()`的输出中隐藏节点,从而防止在节点上发生读操作,即使有第二个读首选项。 | | `members.$.tags` | (可选)文档:这允许您为副本集标记的读取首选项设置标记。 | | `members.$.slaveDelay` | (可选)Integer:这允许您将从服务器设置为比主服务器“延迟”指定的秒数。 | | `members.$.buildIndexes` | (可选)Boolean:该选项用于禁止建立索引。它不应该更好地设置在理论上可以成为主节点节点上。当索引不重要并且您希望节省空间时,此功能对于备份节点等非常有用。 |探索设置结构中可用的选项
表 11-5 列出了Settings结构中可用的副本集属性。这些设置全局应用于整个副本集;您可以使用这些属性来配置副本集成员如何相互通信。
表 11-5。
Inter-server Communication Properties for the Settings Structure
| [计]选项 | 描述 | | --- | --- | | `settings.chainingAllowed` | (可选)Boolean:允许您指定是否允许成员从其他辅助节点复制。默认为真 | | `settings.getLastErrorModes` | (可选)模式:用于设置自定义写入问题,如本章后面所述。 | | `Settings.getLastErrorDefaults` | (可选)默认值:用于设置自定义写入问题 |从应用连接到副本集
从 PHP 连接到副本集类似于连接到单个 MongoDB 实例。唯一的区别是它可以提供一个副本集实例地址或一个副本集成员列表;连接库将确定哪个服务器是主服务器,并将查询定向到该机器,即使主服务器不是您提供的成员之一。因此,无论如何,最好在连接字符串中指定多个成员;这样,您就消除了尝试只从一个可能离线的成员发现的风险。以下示例显示了如何从 PHP 应用连接到副本集:
<?php
$m = new MongoClient("mongodb://localhost:27021,
localhost:27022", array("replicaSet" => "testSet"));
...
?>
在应用中设置阅读首选项
MongoDB 中的读取偏好是一种选择您希望从副本集中读取哪些成员的方式。通过为您的驱动程序指定读取首选项,您可以告诉它对副本集的特定成员(或多个成员)运行查询。目前有五种模式可以在您的驱动程序上设置为读取偏好,如表 11-6 中所列。
表 11-6。
Read Preference Options
| [计]选项 | 描述 | | --- | --- | | `Primary` | 读取将只针对主节点。如果明确与标记的读取首选项一起使用,此读取首选项将被阻止。这也是默认的读取首选项。 | | `PrimaryPreferred` | 除非没有可用的主节点,否则读取将定向到主节点;那么读取将被定向到辅助节点。 | | `Secondary` | 读取将仅定向到辅助节点。如果没有可用的辅助,此选项将生成一个异常。 | | `SecondaryPreferred` | 除非没有可用的辅助节点,否则读取将定向到辅助节点;则读取将针对主节点。这对应于旧的“slaveOk”辅助读取方法的行为。 | | `Nearest` | 从最近的节点读取,不管它是主节点还是辅助节点。`Nearest`使用网络延迟来确定使用哪个节点。 |Note
如果您设置的读取首选项意味着您的读取可能来自辅助节点,那么您必须意识到这些数据可能不是完全最新的;某些操作可能没有从您的主服务器复制。
您可以在 PHP 中使用setReadPreference()命令对一个连接对象设置读取首选项,如下所示:
<?php
$m = new MongoClient("mongodb://localhost:27021,
localhost:27022", array("replicaSet" => "testSet"));
$m->setReadPreference(MongoClient::RP_SECONDARY_PREFERRED, array());
...
?>
从现在开始,您在该连接上进行的任何查询都将针对集群中的辅助节点运行。您还可以通过向 URI 添加阅读首选项标签来设置阅读首选项。指定了读取首选项nearest的 URI 如下所示:
mongodb://localhost:27021,localhost:27022?readPreference=nearest
从应用内部设置写问题
写关注是一个类似于读偏好的概念。您可以使用写问题来指定该数据在被视为“完整”之前需要安全提交到多少个节点这个测试使用 MongoDB 的 Get Last Error (GLE)机制来检查连接上发生的最后一个错误。您可以设置多种写操作模式,这些模式允许您配置在执行写操作时对其持久性的确定程度。每一个都在表 11-7 中列出。
表 11-7。
MongoDB Write Concern Levels
| [计]选项 | 描述 | | --- | --- | | `W=0`或`Unacknowledged` | 一个一发而不可收拾的作品。将发送写操作,但不会尝试确认它是否已提交。 | | `W=1`或`Acknowledged` | 写操作必须由主要人员确认。这是默认值。 | | `W=` `N`或`Replica Set Acknowledged` | 主节点必须确认写入,N–1 成员必须从主节点复制此写入。此选项更可靠,但如果副本集中的成员存在复制延迟,或者如果由于停机等原因在提交写入时没有足够的成员可用,则可能会导致延迟。 | | `W=Majority` | 写操作必须写入主节点,并由足够多的成员复制,以使集合中的大多数成员都确认该写操作。与`w=n`一样,这可能会在停机期间或存在复制延迟时导致问题。 | | `J=true`或`Journaled` | 可与`w=`写入问题一起使用,以指定写入必须持续到日志才能被视为已确认。 |为了在插入中使用写操作,只需在给定的insert()函数中添加w选项,如下所示:
$col->insert($ document,array ("w" => 1));
这将尝试向我们的集合中插入一个带有确认写入的w=1值的文档。
将标签用于读取偏好和写入关注
除了刚才讨论的“读取首选项”和“写入问题”选项之外,还有另一种方法—标记。这种机制允许您在副本集的成员上设置自定义标记,然后使用这些标记以及您的读取首选项和写入关注设置,以更细粒度的方式指导操作。所以,事不宜迟,我们开始吧。通过将标签添加到副本集配置文件的标签部分,可以在副本集上设置标签。让我们首先将a的sites和b的标签添加到我们的副本集配置中:
conf=rs.conf()
conf.members[0].tags = {site : "a"}
conf.members[1].tags = {site : "b"}
conf.members[2].tags = {site : "a"}
rs.reconfigure(conf)
现在,我们可以检查我们的新配置,您可以看到我们已经将两个站点设置到位;它们在每个配置文件的tags部分中定义。
rs.conf()
{
"_id" : "testset",
"version" : 8,
"members" : [
{
"_id" : 0,
"host" : "Pixl.local:27021",
"tags" : {
"site" : "a"
}
},
{
"_id" : 1,
"host" : "Pixl.local:27022",
"tags" : {
"site" : "b"
}
},
{
"_id" : 2,
"host" : "Pixl.local:27023",
"priority" : 0,
"hidden" : true,
"tags" : {
"site" : "a"
}
},
{
"_id" : 3,
"host" : "Pixl.local:27024",
"arbiterOnly" : true
}
]
}
现在让我们开始使用我们的新标签吧!我们可以设置站点a中副本集的最近成员的读取首选项。
$m->setReadPreference(MongoClient::RP_NEAREST, array( array('site' => 'a'),));
既然我们已经解决了读取偏好,让我们开始关注写入。写问题稍微复杂一些,因为我们首先需要修改我们的副本集配置来添加额外的getLastErrorModes。在这种情况下,我们希望创建一个写问题,说明给定的写操作必须提交到足够多的节点,才能写入到两个不同的站点。这意味着写入必须至少提交到站点a和站点b。要做到这一点,我们需要将getLastErrorModes变量设置为一个文档,该文档包含我们新的写关注点的名称和一个规则,该规则表示我们希望将它写入两个不同的“site”标签。这是按如下方式完成的:
conf = rs.conf()
conf.settings. getLastErrorModes = { bothSites : { "site": 2 } } }
rs.reconfig(conf)
现在我们需要插入我们的文档,并指定我们新的写关注点。
$col->insert($document, array("w" => "bothSites"));
就这么简单。现在,我们可以保证我们的写入同时提交到两个站点!现在,假设我们希望将此设置为对我们的群集进行任何写入的默认写入问题。
conf = rs.conf()
conf.settings.getLastErrorDefaults = { bothSites : 1 } }
rs.reconfig(conf)
现在,我们所做的任何写操作都将使用默认的写关注点bothSites来完成。所以如果我们只是做一个普通的插入!
摘要
MongoDB 提供了一组丰富的工具来实现冗余和健壮的复制拓扑。在本章中,您了解了许多这些工具,包括使用它们的一些原因和动机。您还了解了如何设置许多不同的副本集拓扑。此外,您还学习了如何使用命令行工具和内置的 web 界面来检查复制系统的状态。最后,您学习了如何设置和配置读首选项和写关注点,以确保从正确的位置读取和写入。
请花点时间评估本章中描述的每个选项和功能,以确保在生产环境中尝试使用副本集之前,您构建了最适合您特定需求的副本集。使用 MongoDB 在单台机器上创建测试床非常容易;正如我们在本章中所做的那样。因此,强烈建议您尝试每种方法,以确保您完全了解每种方法的优势和局限性,包括它将如何处理您的特定数据和应用。
十二、分片
Abstract
无论你是在构建下一个脸书还是一个简单的数据库应用,如果它成功了,你可能需要在某个时候扩展你的应用。如果您不想不断更换硬件(或者您开始接近在一个硬件上所能做的极限),那么您将希望使用一种技术,允许您在需要时逐渐增加系统的容量。分片是一种允许您将数据分布在多台机器上的技术,但这种方式类似于一个应用访问一个数据库。
无论你是在构建下一个脸书还是一个简单的数据库应用,如果它成功了,你可能需要在某个时候扩展你的应用。如果您不想不断更换硬件(或者您开始接近在一个硬件上所能做的极限),那么您将希望使用一种技术,允许您在需要时逐渐增加系统的容量。分片是一种允许您将数据分布在多台机器上的技术,但这种方式类似于一个应用访问一个数据库。
MongoDB 实现的分片非常适合基于云的计算平台,非常适合动态的、负载敏感的自动扩展,在这种情况下,您可以根据需要增加容量,在不需要时减少容量。
本章将向您介绍如何在 MongoDB 中实现分片,并查看 MongoDB 分片实现中提供的一些高级功能,如标签分片和散列分片键。
探索分享的需求
当万维网刚刚起步时,网站、用户和网上可用信息的数量都很少。Web 由几千个站点和仅仅几万或几十万用户组成,主要集中在学术和研究社区。在早期,数据往往很简单:手工维护的 HTML 文档通过超链接连接在一起。组成 Web 的协议的最初设计目标是提供一种方法,为存储在互联网上不同服务器上的文档创建可导航的引用。
即使是目前的大品牌,如雅虎!与今天的产品相比,在网络上的存在微不足道。该公司最初的产品是雅虎目录,只不过是一个手工编辑的热门网站链接网络。这些链接是由一小群热情的冲浪者维护的。Yahoo 目录中的每个页面都是一个简单的 HTML 文档,存储在文件系统目录树中,并使用简单的文本编辑器进行维护。
但是随着网络的规模开始爆炸式增长——网站和访问者的数量开始近乎垂直地攀升——大量的可用资源迫使早期的网络先驱从简单的文档转向从独立的数据存储中生成更复杂的动态页面。
搜索引擎开始在网络上爬行,将链接的数据库聚集在一起,如今这些数据库拥有数千亿个链接和数百亿个存储页面。
这些发展促使人们转向通过不断发展的内容管理系统来管理和维护数据集,这些内容管理系统主要存储在数据库中以便于访问。
与此同时,新类型的服务不断发展,不仅仅存储文档和链接集。例如,音频、视频、事件和各种其他数据开始进入这些巨大的数据存储库。这一过程通常被描述为“数据工业化”——在许多方面,它与 19 世纪以制造业为中心的工业革命有相似之处。
最终,每个成功的网络公司都面临着如何访问存储在这些庞大数据库中的数据的问题。他们发现一台数据库服务器每秒只能处理这么多的查询,网络接口和磁盘驱动器每秒只能在 web 服务器之间传输这么多兆字节。提供基于 web 的服务的公司会很快发现自己已经超越了单个服务器、网络或驱动器阵列的性能。在这种情况下,他们被迫分割和分发他们收集的大量数据。通常的解决方案是将这些庞大的数据块分割成更小的块,以便更可靠、更快速地管理。同时,这些公司需要保持跨其大型机器集群中保存的全部数据执行操作的能力。
您在第 11 章中详细了解了复制,它是克服这些扩展问题的有效工具,使您能够在多台服务器上创建多个相同的数据副本。这使您能够(在正确的情况下)将服务器负载分散到更多的机器上。
然而,没过多久,您就遇到了另一个问题,组成数据集的各个表或集合变得如此之大,以至于它们的大小超出了单个数据库系统有效管理它们的能力。例如,脸书宣称每天接收超过 3 亿张照片!这个网站已经运营了将近 10 年。
一年之内有 1095 亿张照片,在一张表中包含这么多数据是不可行的。因此,脸书,像他们之前的许多公司一样,寻找将那组记录分布在大量数据库服务器上的方法。脸书采用的解决方案是现实世界中更好记录(和公开)的分片实现之一。
划分水平和垂直数据
数据分区是将数据分割到多个独立数据存储库的机制。这些数据存储可以是共存的(在同一系统上)或远程的(在不同的系统上)。共驻分区的动机是减小单个索引的大小,并减少更新记录所需的 I/O 数量。远程分区的动机是增加访问数据的带宽,方法是使用更多的 RAM 来存储数据,避免磁盘访问,或者提供更多的网络接口和磁盘 I/O 通道。
垂直划分数据
在传统的数据库视图中,数据存储在行和列中。垂直分区包括在列边界上分解记录,并将各部分存储在单独的表或集合中。可以说,使用具有一对一关系的连接表的关系数据库设计是共存垂直数据分区的一种形式。
然而,MongoDB 并不适合这种形式的分区,因为它的记录(文档)结构并不适合整洁的行列模型。因此,很少有机会根据列边界完全分隔一行。MongoDB 还促进了嵌入式文档的使用,并且它不直接支持在服务器上将相关集合连接在一起的能力(这些可以在您的应用中完成)。
水平划分数据
使用 MongoDB 时,水平分区是唯一的选择,而分片是一种流行的水平分区形式的通用术语。分片允许您将集合分割到多个服务器上,以提高包含大量文档的集合的性能。
一个简单的分片示例是将用户记录集合划分到一组服务器上,这样姓氏以字母 A-G 开头的人的所有记录都在一台服务器上,H-M 在另一台服务器上,依此类推。分割数据的规则被称为分片键。
简单地说,分片允许您将分片云视为单个集合,应用不需要知道数据分布在多台机器上。传统的分片实现要求应用主动确定特定文档存储在哪个服务器上,以便正确地路由其请求。传统上,有一个库绑定到应用,这个库负责存储和查询分片数据集中的数据。
MongoDB 有一个独特的分片方法,其中 MongoS 路由进程管理数据的分割和请求到所需分片服务器的路由。如果一个查询需要来自多个分片的数据,那么 MongoS 将管理将从每个分片获得的数据合并回单个游标的过程。
这个特性比其他任何特性都更能让 MongoDB 成为一个云或面向 web 的数据库。
分析简单的共享场景
让我们假设您想要为一个虚构的盖尔语社交网络实现一个简单的分片解决方案。图 12-1 显示了该应用如何分片的简化表示。

图 12-1。
Simple sharding of a User collection
这个简化的应用视图存在许多问题。让我们看看最明显的。
首先,如果您的盖尔语网络面向世界各地的爱尔兰和苏格兰社区,那么数据库将有大量以 Mac 和 Mc 开头的姓名(MacDonald、McDougal 等)用于苏格兰人口,以 O' (O'Reilly、O'Conner 等)用于爱尔兰人口。因此,使用基于姓氏首字母的简单分片键会在支持字母范围“M–o”的分片上放置过多的用户记录。类似地,支持字母范围“X–Z”的分片将执行很少的工作。
分片系统的一个重要特征是,它必须确保数据均匀地分布在可用的一组分片服务器上。这可以防止影响群集整体性能的热点发展。让我们称这个需求为 1:在所有分片中均匀分布数据的能力。
另一件要记住的事情是:当您将数据集分割到多个服务器上时,您实际上增加了数据集对硬件故障的脆弱性。也就是说,当您添加服务器时,单个服务器故障影响数据可用性的可能性会增加。同样,可靠的分片系统的一个重要特征是——像通常与磁盘驱动器一起使用的 RAID 系统一样——它将每个数据块存储在多个服务器上,并且它可以容忍单个分片服务器变得不可用。让我们称这个需求为 2:以容错方式存储分片数据的能力。
最后,您希望确保可以从一组分片中添加或删除服务器,而不必备份和恢复数据,并在一组更小或更大的分片中重新分配数据。此外,您需要能够在不导致集群停机的情况下做到这一点。让我们称这个需求为 3:在系统运行时添加或删除分片的能力。
接下来的章节将介绍如何解决这些需求。
用 MongoDB 实现分片
MongoDB 使用代理机制来支持分片(见图12-2);提供的mongos守护进程充当多个基于mongod的分片服务器的控制器。您的应用附加到mongos进程,就好像它是一个 MongoDB 数据库服务器;此后,您的应用将其所有命令(比如更新、查询和删除)发送给那个mongos进程。

图 12-2。
A simple sharding setup without redundancy
mongos进程负责管理哪个 MongoDB 服务器从您的应用接收命令,这个守护进程将跨多个分片向多个服务器重新发出查询,并将结果聚合在一起。
MongoDB 在集合级别实现分片,而不是在数据库级别。在许多系统中,只有一两个集合可能会增长到需要分片的程度。因此,应该明智地使用分片;如果不需要,您不希望为较小的集合增加管理数据分布的开销。
让我们回到虚构的盖尔语社交网络的例子。在这个应用中,user集合包含了关于用户及其个人资料的详细信息。这个集合可能会增长到需要分片的程度。然而,其他集合,如events、countries和states,不太可能变得如此之大,以至于分片不会带来任何好处。
分片系统使用分片键将数据映射成块,块是文档键的逻辑连续范围(参见第 5 章了解更多关于块的信息)。每个组块识别具有特定连续范围的分片键值的多个文档;这些值使mongos控制器能够快速找到包含它需要处理的文档的块。然后,MongoDB 的分片系统将这些数据块存储在一个可用的分片存储中;配置服务器跟踪哪个块存储在哪个分片服务器上。这是实现的一个重要特性,因为它允许您在集群中添加和删除分片,而无需备份和恢复数据。
当您向集群中添加一个新的分片时,系统将在新的服务器集合中迁移大量的分片,以便均匀地分布它们。类似地,当您删除一个分片时,分片控制器将从脱机的分片中排出数据块,并将它们重新分配给剩余的分片服务器。
MongoDB 的分片设置还需要一个地方来存储其分片的配置,以及一个地方来存储集群中每个分片服务器的信息。为此,需要一个名为 config server 的 MongoDB 服务器;这个服务器实例是一个以特殊角色运行的mongod服务器。如前所述,配置服务器还充当允许确定每个块的位置的目录。集群中可以有一个(开发)或三个(生产)配置服务器。我们总是建议在生产环境中运行三个配置服务器,因为配置服务器的丢失将意味着您不再能够确定您的分片数据的哪些部分在哪些分片上!
乍一看,似乎实现一个依赖于分片的解决方案需要大量的服务器!然而,您可以在数量相对较少的物理服务器上共同托管创建分片设置所需的每种不同服务的多个实例(类似于您在第十一章关于复制的介绍中看到的),但是您需要实现严格的资源管理,以避免 MongoDB 进程相互竞争 RAM 等资源。图 12-3 显示了一个完全冗余的分片系统,它使用副本集作为分片存储和配置服务器,以及一组mongos来管理集群。它还展示了如何将这些服务压缩到仅在三台物理服务器上运行。
小心地放置 shard 存储实例,使它们正确地分布在物理服务器中,这样可以确保您的系统能够承受集群中一个或多个服务器的故障。这反映了 RAID 磁盘控制器在条带中的多个驱动器之间分发数据的方法,使 RAID 配置能够从故障驱动器中恢复。

图 12-3。
A redundant sharding configuration
设置共享配置
为了有效地使用分片,理解它的工作原理是很重要的。下一个例子将带您在一台机器上设置一个测试配置。你将像图 12-2 所示的简单分片系统一样配置这个例子,有两个不同之处:这个例子将通过只使用两个分片来保持简单,并且这些分片将是单个的mongod而不是完整的副本集。最后,您将学习如何创建一个分片集合和一个演示如何使用这个集合的简单 PHP 测试程序。
在这个测试配置中,您将使用表 12-1 中列出的服务。
表 12-1。
Server Instances in the Test Configuration
| 服务 | 守护进程 | 港口 | 数据库路径 | | --- | --- | --- | --- | | 分片控制器 | `mongos` | Twenty-seven thousand and twenty-one | 不适用的 | | 配置服务器 | `mongod` | Twenty-seven thousand and twenty-two | `/db/config/data` | | 沙尔多 | `mongod` | Twenty-seven thousand and twenty-three | `/db/shard1/data` | | Shard1 | `mongod` | Twenty-seven thousand and twenty-four | `/db/shard2/data` |让我们从设置配置服务器开始。为此,请打开一个新的终端窗口,并键入以下代码:
$ mkdir -p /db/config/data
$ mongod --port 27022 --dbpath /db/config/data --configsvr
一旦您启动并运行了配置服务器,请确保您的终端窗口是打开的,或者随意将-–fork和–-logpath选项添加到您的命令中。接下来,你需要设置分片控制器(mongos)。为此,请打开一个新的终端窗口,并键入以下内容:
$ mongos --configdb localhost:27022 --port 27021 --chunkSize 1
这将启动 shard 控制器,它应该会宣布正在监听端口 27021。如果您查看配置服务器的终端窗口,您应该看到 shard 服务器已经连接到它的配置服务器并向它注册了自己。
在本例中,您将块大小设置为可能的最小大小,1MB。对于现实世界的系统来说,这不是一个实用的值,因为这意味着块存储小于文档的最大大小(16MB)。然而,这只是一个演示,小块大小允许您创建许多块来练习分片设置,而不必加载大量数据。默认情况下,chunkSize设置为 64MB,除非另有说明。
最后,您已经准备好启动两台 shard 服务器。为此,您需要两个新的终端窗口,每个服务器一个。在一个窗口中键入以下内容,启动第一台服务器:
$ mkdir -p /db/shard0/data
$ mongod --port 27023 --dbpath /db/shard0/data
并在第二个窗口中键入以下内容,以打开第二台服务器:
$ mkdir -p /db/shard1/data
$ mongod --port 27024 --dbpath /db/shard1/data
您已经启动并运行了服务器。接下来,您需要告诉分片系统分片服务器的位置。为此,您需要使用服务器的主机名连接到您的分片控制器(mongos)。您可以使用 localhost,但是这将您的集群的可伸缩性限制在这台机器上。在运行下面的示例时,您应该用自己的主机名替换<hostname>标记。重要的是要记住,即使mongos不是一个完整的 MongoDB 实例,它对您的应用来说也是一个完整的实例。因此,您可以使用mongo命令 shell 连接到 shard 控制器并添加您的两个 shard,如下所示:
$``mongo``<hostname>
> sh.addShard("``<hostname>
{ "shardAdded" : "shard0000", "ok" : 1 }
> sh.addShard( "``<hostname>
{ "shardAdded" : "shard0001", "ok" : 1 }
您的两台 shard 服务器现已激活;接下来,您需要使用listshards命令检查分片:
> db.printShardingStatus();
--- Sharding Status ---
sharding version: {
"_id" : 1,
"version" : 3,
"minCompatibleVersion" : 3,
"currentVersion" : 4,
"clusterId" : ObjectId("5240282df4ee9323185c70b2")
}
shards:
{ "_id" : "shard0000", "host" : "<hostname>:27023" }
{ "_id" : "shard0001", "host" : "<hostname>:27024" }
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }
您现在有了一个可工作的分片环境,但是没有分片数据;接下来,您将创建一个名为testdb的新数据库,然后在这个数据库中激活一个名为testcollection的集合。您将切分这个集合,因此您将为这个集合提供一个名为testkey的条目,您将使用它作为切分函数:
> sh.enableSharding("testdb")
{ "ok" : 1 }
> sh.shardCollection("testdb.testcollection", {testkey : 1})
{ "collectionsharded" : "testdb.testcollection", "ok" : 1 }
到目前为止,您已经创建了一个带有两个分片存储服务器的分片集群。您还在其上创建了一个带有分片集合的数据库。一个没有任何数据的服务器对任何人来说都是没有用的,所以是时候将一些数据放入这个集合了,这样你就可以看到分片是如何分布的。
为此,您将使用一个小的 PHP 程序来加载带有一些数据的分片集合。您将加载的数据由一个名为testkey的字段组成。这个字段包含一个随机数和第二个字段,其中有一个固定的文本块(第二个字段的目的是确保您可以创建一个合理数量的文本块进行切分)。这个集合是一个名为 TextAndARandomNumber.com 的虚构网站的主数据表。以下代码创建了一个 PHP 程序,将数据插入到您的分片服务器中:
<?php
// Open a database connection to the mongos daemon
$mongo = new MongoClient("localhost:27021");
// Select the test database
$db = $mongo->selectDB('testdb');
// Select the TestIndex collection
$collection = $db->testcollection;
for($i=0; $i < 100000 ; $i++){
$data=array();
$data['testkey'] = rand(1,100000);
$data['testtext'] = "Because of the nature of MongoDB, many of the more "
. "traditional functions that a DB Administrator "
. "would perform are not required. Creating new databases, "
. "collections and new fields on the server are no longer necessary, "
. "as MongoDB will create these elements on-the-fly as you access them."
. "Therefore, for the vast majority of cases managing databases and "
. "schemas is not required.";
$collection->insert($data);
}
这个小程序会连接到 shard 控制器(mongos)上,用随机testkeys和一些testtext插入 100000 条记录来填充文档。如前所述,这个示例文本导致这些文档占据足够多的块,使得使用分片机制变得可行。
以下命令运行测试程序:
$php testshard.php
一旦程序完成运行,您可以用命令 shell 连接到mongos实例,并验证数据是否已经存储:
$mongo localhost:27021
>use testdb
>db.testcollection.count()
100000
此时,您可以看到您的服务器已经存储了 100,000 条记录。现在,您需要连接到每个分片,并查看每个分片在testdb.testcollection中存储了多少项。下面的代码使您能够连接到第一个分片,并查看有多少来自testcollection集合的记录存储在其中:
$mongo localhost:27023
>use testdb
>db.testcollection.count()
48875
这段代码使您能够连接到第二个分片,并查看有多少来自testcollection集合的记录存储在其中:
$mongo localhost:27024
>use testdb
>db.testcollection.count()
51125
Note
您可能会看到每个分片中文档数量的不同值,这取决于您查看单个分片的确切时间。mongos实例最初可能会将所有的块放在一个分片上,但是随着时间的推移,它会通过移动块来重新平衡分片集,从而在所有的分片之间均匀地分布数据。因此,存储在给定分片中的记录数量可能会随时变化。这满足了“需求 1:跨所有分片均匀分布数据的能力”
向集群添加新分片
让我们假设商业真的在 TextAndARandomNumber.com 跳跃。为了满足需求,您决定向集群中添加一个新的 shard 服务器,以进一步分散负载。
添加新分片很容易;你只需要重复前面描述的步骤。首先创建新的 shard 存储服务器,并将其放在端口 27025 上,这样它就不会与您现有的服务器发生冲突:
$ sudo mkdir -p /db/shard2/data
$ sudo mongod --port 27025 --dbpath /db/shard2/data
接下来,您需要将新的 shard 服务器添加到集群中。您可以登录到分片控制器(mongos),然后使用管理命令addshard:
$mongo localhost:27021
>sh.addShard("localhost:27025")
{ "shardAdded" : "shard0002", "ok" : 1 }
此时,您可以运行listshards命令来验证分片已经被添加到集群中。这样做揭示了一个新的分片服务器(shard2)现在出现在shards数组中:
> db.printShardingStatus();
--- Sharding Status ---
sharding version: {
"_id" : 1,
"version" : 3,
"minCompatibleVersion" : 3,
"currentVersion" : 4,
"clusterId" : ObjectId("5240282df4ee9323185c70b2")
}
shards:
{ "_id" : "shard0000", "host" : "<hostname>:27023" }
{ "_id" : "shard0001", "host" : "<hostname>:27024" }
{ "_id" : "shard0002", "host" : "<hostname>:27025" }
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }
如果您登录到您在端口 27025 上创建的新 shard 存储服务器并查看testcollection,您将看到一些有趣的东西:
$mongo localhost:27025
> use testdb
switched to db testdb
> show collections
system.indexes
testcollection
> db.testcollection.count()
4657
> db.testcollection.count()
4758
> db.testcollection.count()
6268
这表明新的shard2存储服务器上的testcollection中的项目数量正在慢慢增加。您所看到的证明了分片系统正在扩展的群集中重新平衡数据。随着时间的推移,分片系统将从shard0和shard1存储服务器迁移数据块,以在组成集群的三台服务器之间创建均匀的数据分布。这个过程是自动的,即使没有新数据插入到testcollection集合中,它也会发生。在这种情况下,mongos shard 控制器将块移动到新的服务器,然后将它们注册到配置服务器。
这是选择块大小时要考虑的因素之一。如果你的chunkSize值非常大,你将得到一个不太均匀的数据分布;相反,您的chunkSize值越小,您的数据分布就越均匀。
从集群中移除分片
当它持续的时候,它是伟大的,但是现在假设 TextAndARandomNumber.com 是昙花一现,它的光芒已经失败了。经过几周的疯狂活动后,网站的流量开始下降,所以你不得不开始寻找降低运营成本的方法——换句话说,新的分片服务器必须淘汰!
在下一个例子中,您将删除之前添加的 shard 服务器。要启动这个过程,登录到分片控制器(mongos)并发出removeShard命令:
$ mongo localhost:27021
> use admin
switched to db admin
> db.runCommand({removeShard : "localhost:27025"})
{
"msg" : "draining started successfully",
"state" : "started",
"shard" : "shard0002",
"ok" : 1
}
removeShard命令响应一条消息,指示移除过程已经开始。它还表明mongos已经开始将目标分片服务器上的块重新定位到集群中的其他分片服务器。这个过程被称为耗尽分片。
您可以通过重新发出removeShard命令来检查排水过程的进度。响应将告诉您还有多少块和数据库需要从分片中排出:
> db.runCommand({removeShard : "localhost:27025"})
{
"msg" : "draining ongoing",
"state" : "ongoing",
"remaining" : {
"chunks" : NumberLong( 12 ),
"dbs" : NumberLong( 0 )
},
"ok" : 1
}
最后,removeShard进程将终止,您将收到一条消息,指示删除进程已完成:
> db.runCommand({removeShard : "localhost:27025"})
{
"msg" : "removeshard completed successfully",
"state" : "completed",
"shard" : "shard0002",
"ok" : 1
}
为了验证removeShard命令是否成功,您可以运行listshards来确认所需的 shard 服务器已经从集群中删除。例如,以下输出显示您之前创建的shard2服务器不再列在shards数组中:
>db.runCommand({listshards:1})
{
"shards" : [
{
"_id" : "shard0000",
"host" : "localhost:27023"
},
{
"_id" : "shard0001",
"host" : "localhost:27024"
}
],
"ok" : 1
}
此时,您可以终止 Shard2 mongod进程并删除它的存储文件,因为它的数据已经被迁移回其他服务器。
Note
在不使集群离线的情况下向集群添加和从集群中删除分片的能力是 MongoDB 支持高可伸缩性、高可用性、大容量数据存储的能力的关键组成部分。这满足了最终的需求:“需求 3:在系统运行时添加或移除分片的能力。”
确定您的联系方式
您的应用可以连接到标准的非共享数据库(mongod)或共享控制器(mongos)。MongoDB 实现了这两个过程;除了少数用例,数据库和分片控制器的外观和行为完全相同。但是,有时确定您连接的系统类型可能很重要。
MongoDB 提供了isdbgrid命令,您可以使用它来询问连接的数据系统,以确定它是否被分片。下面的代码片段显示了如何使用这个命令,以及它的输出是什么样子的:
$mongo
>use testdb
>db.runCommand({ isdbgrid : 1});
{ "isdbgrid" : 1, "hostname" : "Pixl.local", "ok" : 1 }
该响应包括isdbgrid:1字段,它告诉您所连接的数据库支持分片。对isdbgrid:0的响应表明您连接到了一个非共享的数据库。
列出分片集群的状态
MongoDB 还包括一个简单的命令,用于转储分片集群的状态:printShardingStatus()。
这个命令可以让您深入了解分片系统的内部。下面的代码片段显示了如何调用printShardingStatus()命令,但是去掉了一些返回的输出以便于阅读:
$mongo localhost:27021
>sh.status();
--- Sharding Status ---
sharding version: {
"_id" : 1,
"version" : 3,
"minCompatibleVersion" : 3,
"currentVersion" : 4,
"clusterId" : ObjectId("51c699a7dd9fc53b6cdc4718")
}
shards:
{ "_id" : "shard0000", "host" : "localhost:27023" }
{ "_id" : "shard0001", "host" : "localhost:27024" }
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }
{ "_id" : "testdb", "partitioned" : true, "primary" : "shard0000" }
testdb.testcollection
shard key: { "testkey" : 1 }
chunks:
shard0000 2
shard0001 3
{ "testkey" : { "$minKey" : 1 } } -->> { "testkey" : 0 } on : shard0000 Timestamp(4, 0)
{ "testkey" : 0 } -->> { "testkey" : 14860 } on : shard0000 Timestamp(3, 1)
{ "testkey" : 14860 } -->> { "testkey" : 45477 } on : shard0001 Timestamp(4, 1)
{ "testkey" : 45477 } -->> { "testkey" : 76041 } on : shard0001 Timestamp(3, 4)
{ "testkey" : 76041 } -->> { "testkey" : { "$maxKey" : 1 } } on : shard0001 Timestamp(3, 5)
该输出列出了分片服务器、每个分片数据库/集合的配置以及分片数据集中的每个块。因为您使用了一个小的chunkSize值来模拟一个更大的分片设置,所以这个报告列出了很多块。从这个清单中可以获得的一条重要信息是与每个块相关联的分片键的范围。输出还显示了特定块存储在哪个 shard 服务器上。您可以使用该命令返回的输出作为工具的基础,来分析 shard 服务器的键和块的分布。例如,您可以使用此数据来确定数据集中是否有任何数据聚集。
使用副本集实现分片
到目前为止,您所看到的例子依赖于一个单独的mongod实例来实现每个分片。在《??》第 11 章中,你学习了如何创建副本集,副本集是由mongod个实例组成的集群,它们一起工作以提供冗余和故障安全存储。
当向分片集群添加分片时,您可以提供一个副本集的名称和该副本集成员的地址,该分片将在每个副本集成员上被实例化。Mongos 将跟踪哪个实例是副本集的主服务器;它还将确保对该实例进行所有的分片写入。
将分片和副本集结合起来,使您能够创建高性能、高可靠性的集群,能够容忍多机故障。它还使您能够最大限度地提高廉价的商品级硬件的性能和可用性。
Note
使用副本集作为分片存储机制的能力满足了“需求 2:以容错方式存储分片数据的能力”
平衡
我们之前已经讨论过 MongoDB 如何自动将您的工作负载分布在集群中的所有分片上。虽然您可能认为这是通过某种形式的专利 MongoDB-Magic 实现的,但事实并非如此。您的 MongoS 进程中有一个称为平衡器的元素,它在您的集群中移动数据的逻辑块,以确保它们均匀地分布在您的所有分片中。平衡器与分片对话,告诉它们将数据从一个分片迁移到另一个分片。在下面的例子中,您可以看到sh.status()输出中块的分布。您可以看到,我的数据在shard0000上分为两个区块,在shard0001上分为三个区块。
{ "_id" : "testdb", "partitioned" : true, "primary" : "shard0000" }
testdb.testcollection
shard key: { "testkey" : 1 }
chunks:
shard0000 2
shard0001 3
虽然平衡器代表您自动完成所有这些工作,但您确实可以决定它何时运行。您可以根据需要停止和启动平衡器,并设置它可以运行的窗口。要停止平衡器,您需要连接到 MongoS 并发出sh.stopBalancer()命令:
> sh.stopBalancer();
Waiting for active hosts...
Waiting for the balancer lock...
Waiting again for active hosts after balancer is off...
如你所见,平衡器现在关闭了;该命令已将平衡器状态设置为off,并等待和确认平衡器已完成所有正在运行的迁移。启动平衡器也是同样的过程;我们运行sh.startBalancer()命令:
> sh.startBalancer();
现在,这两个命令可能需要一些时间来完成和返回,因为它们都在等待确认平衡器是否启动并实际运行。如果您遇到困难或希望自己手动确认状态,您可以执行以下检查。首先,您可以检查平衡器标志设置为什么。这是作为平衡器开/关开关的文档,它位于配置数据库中。
> use config
switched to db config
db.settings.find({_id:"balancer"})
{ "_id" : "balancer", "stopped" : true }
现在您可以看到这里的文档,其_id值为balancer被设置为stopped : true,这意味着平衡器没有运行(停止)。然而,这并不意味着还没有迁移在运行;为了证实这一点,我们需要检查“平衡器锁”
平衡器锁的存在是为了确保在给定时间只有一个平衡器可以执行平衡操作。您可以使用以下命令找到平衡器锁:
> use config
switched to db config
> db.locks.find({_id:"balancer"});
{ "_id" : "balancer", "process" : "Pixl.local:40000:1372325678:16807", "state" : 0, "ts" : ObjectId("51cc11c57ce3f0ee9684caff"), "when" : ISODate("2013-06-27T10:19:49.820Z"), "who" : "Pixl.local:40000:1372325678:16807:Balancer:282475249", "why" : "doing balance round" }
您可以看到这是一个比设置文档复杂得多的文档。然而,最重要的是state条目,它表示锁是否被占用,0 表示“空闲”或“未被占用”,其他的表示“正在使用”您还应该注意时间戳,它表示锁被取出的时间。将刚刚显示的“自由”锁与接下来的“被占用”锁进行比较,这表明平衡器是活动的。
> db.locks.find({_id:"balancer"});
{ "_id" : "balancer", "process" : "Pixl.local:40000:1372325678:16807", "state" : 1, "ts" : ObjectId("51cc11cc7ce3f0ee9684cb00"), "when" : ISODate("2013-06-27T10:19:56.307Z"), "who" : "Pixl.local:40000:1372325678:16807:Balancer:282475249", "why" : "doing balance round" }
现在,您知道了如何启动和停止平衡器,以及如何检查平衡器在某个给定点正在做什么。您还将希望能够设置一个窗口,当平衡器将被激活。例如,让我们将我们的平衡器设置为在晚上 8 点到早上 6 点之间运行,这样,当我们的集群(假设)不太活跃时,它可以整夜运行。为此,我们更新了之前的平衡器设置文档,因为它控制平衡器是否正在运行。交换看起来是这样的:
> use config
switched to db config
>db.settings.update({_id:"balancer"}, { $set : { activeWindow : { start : "20:00", stop : "6:00" } } }
这就够了。您的平衡器文档现在将有一个activeWindow,它将在晚上 8 点启动,在早上 6 点停止。现在,您应该能够启动和停止平衡器,确认其状态以及上次运行的时间,最后设置平衡器处于活动状态的时间窗口。
散列分片密钥
前面我们讨论了选择正确的分片密钥有多重要。如果选择了错误的分片键,可能会导致各种性能问题。以_id上的分片为例,它是一个不断增加的值。您所做的每个插入都将被发送到您的集合中当前拥有最高_id值的分片。因为每个新插入的值都是已经插入的“最大”值,所以您将总是在相同的位置插入数据。这意味着您的集群中有一个“热”分片,它接收所有插入,并将所有文档从它迁移到其他分片——效率不是很高。
为了解决这个问题,MongoDB 2.4 引入了一个新特性——散列分片键!散列分片键将为给定字段上的每个值创建一个散列,然后使用这些散列来执行分块和分片操作。这允许您获取一个递增的值,比如一个_id字段,并为每个 given _id 值生成一个散列,这将赋予值随机性。添加这种级别的随机性通常可以让您将写操作平均分配到所有分片上。然而,代价是您还会有随机读取,如果您希望对一系列文档执行操作,这可能会降低性能。因此,在某些工作负载下,与用户选择的分片密钥相比,哈希分片可能效率较低。
Note
由于哈希的实现方式,在对浮点(十进制)数进行分片时会有一些限制,这意味着像 2.3、2.4 和 2.9 这样的值将成为相同的哈希值。
因此,要创建散列分片,我们只需运行shardCollection并创建一个"hashed"索引!
sh.shardCollection( " testdb.testhashed", { _id: "hashed" } )
就这样!现在您已经创建了一个散列分片键,它将散列传入的_id值,以便以更“随机”的方式分发您的数据。现在,记住所有这些,你们中的一些人可能会说——为什么不总是使用散列分片密钥呢?
好问题;答案是分片只是“那些”黑魔法中的一种。最佳的分片键是一个允许你的写操作被很好地分布在多个分片上的键,所以写操作是有效并行的。它也是一个键,允许您进行分组,以便只对一个或有限数量的分片进行写入,并且它必须允许您更有效地利用单个分片上的索引。所有这些因素都将由您的使用情形、您存储的内容以及检索方式决定。
标签分片
有时,在某些情况下,说“我希望我能在这个分片上拥有所有的数据”是有意义的。这就是 MongoDB 的标签分片可以大放异彩的地方。您可以设置标签,使 shard 键的给定值指向集群中的特定分片!这一过程的第一步是确定您希望通过标签设置实现的目标。在下一个示例中,我们将完成一个简单的设置,我们希望根据地理位置分布数据,一个位置在美国,另一个在欧盟。
这个过程的第一步是向我们现有的分片中添加一些新的标签。我们用sh.addShardTag函数来做这件事,简单地添加我们的 shard 的名字和我们希望给它的标签。在这个例子中,我将shard0000设为美国分片,将shard0001设为欧盟分片:
> sh.addShardTag("shard0000","US");
> sh.addShardTag("shard0001","EU");
现在,为了查看这些更改,我们可以运行sh.status()命令并查看输出:
> sh.status();
--- Sharding Status ---
sharding version: {
"_id" : 1,
"version" : 3,
"minCompatibleVersion" : 3,
"currentVersion" : 4,
"clusterId" : ObjectId("51c699a7dd9fc53b6cdc4718")
}
shards:
{ "_id" : "shard0000", "host" : "localhost:27023", "tags" : [ "US" ] }
{ "_id" : "shard0001", "host" : "localhost:27024", "tags" : [ "EU" ] }
...
正如你所看到的,我们的分片现在有了美国和欧盟的标签,但是仅仅这些是没有用的;我们需要告诉 MongoS 根据一些规则将我们给定集合的数据路由到这些分片。这就是棘手的地方;我们需要配置我们的分片,以便我们分片的数据包含一些我们可以执行规则评估的内容,以便正确地路由它们。除此之外,我们还想保持和以前一样的分发逻辑。如果你回想一下前面的讨论,你会发现,在大多数情况下,我们只需要让这种按地区的划分在“之前”发生
这里的解决方案是向我们的分片键添加一个额外的键,表示数据所属的区域,并将它作为分片键的第一个元素。因此,现在我们需要切分一个新的集合,以便添加这些标签:
> sh.shardCollection("testdb.testtagsharding", {region:1, testkey:1})
{ "collectionsharded" : "testdb.testtagsharding", "ok" : 1 }
Note
虽然键的标记部分不需要成为第一个元素,但它通常是最好的;这样,组块首先被标签分解。
现在,我们已经设置好了标签,我们已经有了 shard 键,它将把我们的块分成很好的区域块,现在我们所需要的就是规则了!要添加这些,我们使用sh.addTagRange命令。这个命令接受集合的名称空间、最小和最大范围值,以及数据应该发送到的标签。MongoDB 的标记范围是最小包含和最大排除。因此,如果我们想要将值为 EU 的任何内容发送到标记 EU,我们需要一个从 EU 到 EV 的范围。对我们来说,我们想要从我们到 UT 的范围。这为我们提供了以下命令:
> sh.addTagRange("testdb.testtagsharding", {region:"EU"}, {region:"EV"}, "EU")
> sh.addTagRange("testdb.testtagsharding", {region:"US"}, {region:"UT"}, "US")
从现在开始,任何符合这些标准的文档都将被发送到这些分片中。所以为了测试东西,我们来介绍几个文档。我编写了一个短循环,将 10,000 个与我们的 shard 键匹配的文档引入集群。
for(i=0;i<10000;i++){db.getSiblingDB("testdb").testtagsharding.insert({region:"EU",testkey:i})}
现在我们运行sh.status(),可以看到分片分块分解:
testdb.testtagsharding
shard key: { "region" : 1, "testkey" : 1 }
chunks:
shard0000 3
{ "region" : { "$minKey" : 1 }, "testkey" : { "$minKey" : 1 } } -->> { "region" : "EU", "testkey" : { "$minKey" : 1 } } on : shard0000 Timestamp(1, 3)
{ "region" : "EU", "testkey" : { "$minKey" : 1 } } -->> { "region" : "EU", "testkey" : 0 } on : shard0000 Timestamp(1, 4)
{ "region" : "EU", "testkey" : 0 } -->> { "region" : { "$maxKey" : 1 }, "testkey" : { "$maxKey" : 1 } } on : shard0000 Timestamp(1, 2)
tag: EU { "region" : "EU" } -->> { "region" : "EV" }
tag: US { "region" : "US" } -->> { "region" : "UT" }
由此我们可以看出哪些组块在哪里;欧盟分片上有三大块,美国分片上没有。从这些范围中,我们可以看到其中两个块应该是空的。如果你进入每个单独的分片服务器,你会发现我们插入的所有 10,000 个文档都在一个分片上。您可能已经注意到日志文件中的以下消息:
Sun Jun 30 12:11:16.549 [Balancer] chunk { _id: "testdb.testtagsharding-region_"EU"testkey_MinKey", lastmod: Timestamp 1000|2, lastmodEpoch: ObjectId('51cf7c240a2cd2040f766e38'), ns: "testdb.testtagsharding", min: { region: "EU", testkey: MinKey }, max: { region: MaxKey, testkey: MaxKey }, shard: "shard0000" } is not on a shard with the right tag: EU
Sun Jun 30 12:11:16.549 [Balancer] going to move to: shard0001
出现此消息是因为我们已将标记范围设置为仅适用于欧盟和美国的值。鉴于我们现在所知道的,我们可以稍微修改它们,以覆盖所有的标签范围。让我们删除这些标签范围,并添加新的范围;我们可以使用以下命令删除旧文档:
> use config
> db.tags.remove({ns:"testdb.testtagsharding"});
现在我们可以添加回标签,但是这次我们可以从minKey运行到US,从US运行到maxKey,就像前面例子中的块范围一样!为此,使用特殊的MinKey和MaxKey操作符,它们代表分片键范围的最小和最大可能值。
> sh.addTagRange("testdb.testtagsharding", {region:MinKey}, {region:"US"}, "EU")
> sh.addTagRange("testdb.testtagsharding", {region:"US"}, {region:MaxKey}, "US")
现在,如果我们再次运行sh.status(),您可以看到范围;这一次,事情看起来运行得更好:
testdb.testtagsharding
shard key: { "region" : 1, "testkey" : 1 }
chunks:
shard0001 3
shard0000 1
{ "region" : { "$minKey" : 1 }, "testkey" : { "$minKey" : 1 } } -->> { "region" : "EU", "testkey" : { "$minKey" : 1 } } on : shard0001 Timestamp(4, 0)
{ "region" : "EU", "testkey" : { "$minKey" : 1 } } -->> { "region" : "EU", "testkey" : 0 } on : shard0001 Timestamp(2, 0)
{ "region" : "EU", "testkey" : 0 } -->> { "region" : "US", "testkey" : { "$minKey" : 1 } } on : shard0001 Timestamp(3, 0)
{ "region" : "US", "testkey" : { "$minKey" : 1 } } -->> { "region" : { "$maxKey" : 1 }, "testkey" : { "$maxKey" : 1 } } on : shard0000 Timestamp(4, 1)
tag: EU { "region" : { "$minKey" : 1 } } -->> { "region" : "US" }
tag: US { "region" : "US" } -->> { "region" : { "$maxKey" : 1 } }
我们的数据分布得更好,涉及的范围覆盖了从最小值到最大值的整个分片键范围。如果我们进一步向集合中插入条目,它们的数据将被正确地路由到我们想要的分片。没有混乱和大惊小怪。
摘要
分片使您能够扩展您的数据存储,以处理非常大的数据集。它还使您能够根据系统的增长来扩展集群。MongoDB 提供了一个简单的自动分片配置,可以很好地满足大多数需求。尽管这个过程是自动化的,但是您仍然可以对其特性进行微调,以支持您的特定需求。分片是 MongoDB 区别于其他数据存储技术的关键特性之一。阅读本章之后,您应该理解如何在多个 MongoDB 实例上分割您的数据,管理和维护一个分割的集群,以及如何利用标签分割和散列分割键。我们希望这本书能够帮助您了解 MongoDB 的许多设计方式,与使用更传统的数据库工具相比,MongoDB 能够更好地满足现代基于 web 的应用的严格要求。
您在本书中学到的主题包括以下内容:
- 如何在多种平台上安装和配置 MongoDB?
- 如何从各种开发语言访问 MongoDB?
- 如何与围绕产品的社区建立联系,包括如何获得帮助和建议。
- 如何设计和构建利用 MongoDB 独特优势的应用。
- 如何对基于 MongoDB 的数据存储进行优化、管理和故障排除。
- 如何创建跨多台服务器的可伸缩容错安装。
强烈建议您探究本书中提供的许多样本和示例。其他 PHP 示例可以在位于 www.php.net/manual/en/book.mongo.php 的 PHP MongoDB 驱动文档中找到。MongoDB 是一个非常容易接近的工具,其安装和操作的简易性鼓励了实验。所以不要犹豫:转动曲柄,开始玩吧!很快你也会开始欣赏这款迷人产品为你的应用带来的所有可能性。
第一部分:MongoDB 基础知识
第二部分:使用 MongoDB 开发
第三部分:大数据和高级 MongoDB


浙公网安备 33010602011771号