MongoDB-权威指南-二-

121 阅读47分钟

MongoDB 权威指南(二)

原文:The Definitive Guide to Mongodb

协议:CC BY-NC-SA 4.0

五、文件系统

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 的块。规范要求将它们分别命名为fileschunks。默认情况下,fileschunks集合是在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.jpgme.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 版本中的工作方式有了重要的改变,尤其是在getput的工作方式上。

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命令结合使用的两个有用参数是filenamecontent_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, wtimeouttimeoutfsync选项可以设置为TRUEFALSEFALSE是该选项的默认值。如果设置为TRUEfsync在指示插入成功之前,强制将数据写入硬盘。该选项将覆盖选项w的任何设置,将其设置为 0。j选项可以设置为TRUEFALSE,默认为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()函数。下面几节将向您介绍如何使用几种常用的运算符。

使用ltlt、gt、ltelte 和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操作符与任何其他键/值组合结合使用。我们来看两个例子。

第一个示例搜索并返回任何包含整数值为28Age键或值为USAAddress.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 NameLast Name字段将分别被重命名为Given NameFamily 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);

pushpush 和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

指定值-21000不会改变删除哪个元素。任何负数都将删除第一个元素,而任何正数都将删除最后一个元素。使用值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函数有四个参数;queryupdatefieldsoptions。其中一些是可选的,取决于您的操作。例如,当指定update标准时,字段和选项是可选的。然而,当您希望使用remove选项时,需要指定updatefields参数(例如,使用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

)

以下示例显示了如何使用removesort参数:

// 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 驱动程序。