MongoDB 权威指南(一)

原文:The Definitive Guide to Mongodb

协议:CC BY-NC-SA 4.0

一、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 还允许您将查询写成mapreduce函数。如果这听起来像是很大的努力,那么你在一个好公司;CouchDB 的学习曲线有些严峻。公平地说,一个有经验的程序员可能会很快掌握它;然而,对于大多数人来说,学习曲线可能太陡了,以至于他们不愿意使用这个工具。

幸运的是,对于我们这些凡人来说,MongoDB 更容易使用。我们将在整本书中更详细地介绍如何使用 MongoDB,但这里有一个简短的版本:在 MongoDB 中,您只需提供想要匹配的文档部分,MongoDB 会完成其余部分。然而,MongoDB 可以做得更多。比如你想用map或者reduce函数就不会发现 MongoDB 缺少。同时,您可以轻松地使用 MongoDB 您不必预先了解该工具的所有高级特性。

为您的文档编制索引

MongoDB 包括对文档索引的广泛支持,当您处理成千上万的文档时,这个特性真的很方便。如果没有索引,MongoDB 将不得不依次查看每个单独的文档,以确定它是否是您想要查看的内容。这就像向图书管理员要一本书,然后看着他在库里走来走去,看每一本书。有了索引系统(库倾向于使用杜威十进制系统),他可以找到你要找的书所在的区域,并很快确定它是否在那里。

与库的书不同,MongoDB 中的所有文档都在_id键上自动索引。该键被视为特殊情况,因为您不能删除它;索引确保每个值都是唯一的。这个键的好处之一是可以保证每个文档都是唯一可识别的,而 RDBMS 不能保证这一点。

当您创建自己的索引时,您可以决定是否希望它们强制唯一性。如果您决定创建一个惟一的索引,您可以告诉 MongoDB 删除所有重复的索引。这可能是也可能不是您想要的,所以在使用此选项之前您应该仔细考虑,因为您可能会意外删除一半的数据。默认情况下,如果您尝试在具有重复值的键上创建唯一索引,将会返回错误。

在许多情况下,您会希望创建一个允许重复的索引。例如,如果您的应用按姓氏进行搜索,那么在姓氏键上建立索引是有意义的。当然,您不能保证每个姓氏都是唯一的;并且在任何合理大小的数据库中,副本实际上是可以保证的。

然而,MongoDB 的索引能力并不止于此。MongoDB 还可以在嵌入式文档上创建索引。例如,如果您在 address 键中存储了大量地址,则可以根据邮政编码创建一个索引。这意味着您可以基于任何邮政编码轻松地撤回文档,而且速度非常快。

MongoDB 通过允许复合索引更进一步。在复合索引中,两个或多个键用于构建给定的索引。例如,您可以构建一个结合了lastnamefirstname标签的索引。搜索全名会非常快,因为 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 的拥护者嘲笑mapreduce函数的复杂性。这对一些人来说很可怕,因为这些函数需要一种完全不同的方式来寻找和排序数据,许多专业程序员很难理解支撑mapreduce函数的概念。也就是说,这些函数提供了一种极其强大的数据查询方式。事实上,CouchDB 只支持这种方法,这也是它具有如此高的学习曲线的一个原因。

MongoDB 不要求您使用mapreduce函数。事实上,MongoDB 依赖于一个简单的查询语法,更类似于 MySQL 中的语法。然而,MongoDB 确实为那些想要的人提供了这些功能。mapreduce函数是用 JavaScript 编写的,在服务器上运行。map功能的任务是找到所有符合特定标准的文档。这些结果随后被传递给处理数据的reduce函数。reduce函数通常不返回文档集合;相反,它返回一个包含派生信息的新文档。一般来说,如果您通常在 SQL 中使用 GROUP BY,那么mapreduce函数可能是 MongoDB 中的合适工具。

Note

您不应该认为 MongoDB 的mapreduce函数是 CouchDB 所采用方法的拙劣模仿。如果您愿意,可以使用 MongoDB 的mapreduce函数来代替 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/dbC:\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 数据库一起使用。两个“最重要”的应用是mongomongod应用。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` | 显示当前数据库中的用户。 | | `use ` | 将当前数据库设置为``。 |

Tip

您可以通过在 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 版本,扩展目录可能被称为ExtExtensions。如果您不确定应该是哪个目录,您可以查看系统上安装的 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.phpphpinfo.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]

}

在查看这两个文档时,您可能已经注意到,大多数字段彼此并不紧密相关。是的,它们都有名为TitleType的字段;但是除了相似性之外,这些文档是完全不同的。尽管如此,这两个文件都包含在一个叫做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:该数据类型可以设置为TRUEFALSE
  • 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参数可以用来指定文档的对象类型,可以是PointLineStringPolygon。正如所料,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()假设给出了一个纬度/经度键,它使用的范围是从-180180。但是,您可以使用minmax参数覆盖这些值:

> 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 包括几个函数,您可以使用它们来更精确地控制您的查询。我们将在本节讲述如何使用sortlimitskip功能。

您可以使用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 附带了一组很好的聚合命令。一开始您可能看不到它们的重要性,但是一旦您掌握了它们,您将会看到聚合命令形成了一套极其强大的工具。例如,您可以使用它们来获得关于数据库的一些基本统计信息的概述。在这一节中,我们将仔细研究如何使用可用的聚合命令中的三个函数:countdistinctgroup

除了这三个基本的聚合命令,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()函数有三个参数:keyinitialreduce

key参数指定您想要分组的结果。例如,假设您想按Title对结果进行分组。initial参数允许您为每个分组结果提供一个基数(也就是开始时项目的基数)。默认情况下,如果希望返回一个精确的数字,可以将该参数设置为零。reduce参数将所有相似的项目组合在一起。Reduce 有两个参数:正在迭代的当前文档和聚合计数器对象。在下面的例子中,这些参数被称为itemsprev。从本质上来说,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

}

]

除了keyinitialreduce参数外,您还可以指定三个可选参数:

  • 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()函数有三个主要参数:criteriaobjNewoptions

criteria参数允许您指定选择想要更新的记录的查询。您使用objNew参数来指定更新的信息;或者你可以使用一个运营商来为你做这件事。options参数允许您在更新文档时指定选项,有两个可能的值:upsertmultiupsert选项让您指定更新是否应该是 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修饰符操作符的同时添加upsertmulti选项,如下所示:

> 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")

}

在数组中指定多个值

使用数组时, p u s h 操作符将把指定的值附加到给定的数组中,扩展存储在给定元素中的数据。如果您希望将几个单独的值添加到给定的数组中,可以使用可选的 ‘ push 操作符将把指定的值附加到给定的数组中,扩展存储在给定元素中的数据。如果您希望将几个单独的值添加到给定的数组中,可以使用可选的` 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

指定值-21000不会改变删除哪个元素。任何负数都将删除第一个元素,而任何正数都将删除最后一个元素。使用数字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 问题。这种情况似乎不太可能,但是在多用户环境中,许多应用同时处理数据,这可能是一个严重的问题。

要避免此问题,您可以执行下列操作之一:

  • 在更新的查询表达式中使用整个对象,而不仅仅是_idcomments.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()函数返回truefalse。同样,如果想从 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)

您需要的所有信息已经分别插入到了publisherscollectionmedia集合中。您现在可以开始使用数据库参考。首先,将包含发布者信息的文档指定给一个变量:

> 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 的基础知识,包括它是什么,它做什么,以及如何使用它为您带来好处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值