锻造车间 - MongoDB

作者:Reuven M. Lerner

最近,我一直在教授 Python 和 Ruby 编程课程,学员通常是习惯了 C++ 和 Java 的经验丰富的开发人员。不可避免地,Python 和 Ruby 是动态类型语言,允许任何变量包含任何类型的值,这一事实让这些学生感到惊讶。他们经常震惊地发现,程序中的给定变量可以在任何时候被赋值为包含整数、字符串或对象实例,没有任何约束。他们想知道,鉴于运行时类型错误的可能性,怎么会有人(或愿意)使用这样的语言。作为这门课程的讲师,我的工作之一是让他们相信在这种语言中工作是可能的,但这样做可能需要比他们习惯的更遵守约定。

因此,具有讽刺意味的是,在过去的几个月中,当我开始尝试非关系型数据库时,我发现自己经历了类似于我的学生所受到的冲击。我对数据完整性以及什么构成可靠数据库的长期信念经历了一点动摇。我仍然对这些非关系型(或 NoSQL)数据库有些警惕,并且远未确信现在是时候抛弃 SQL 和关系模型,转而支持通常更容易使用的东西了。

我确实认为,正如我在上个月的专栏中概述的那样,这些数据库提供了一种存储和检索类型,这种类型通常更自然地适合许多数据存储需求。而且,正如 memcached 提供了一种补充关系数据库而不是取代关系数据库的替代存储系统一样,这些非关系型数据库也可以执行许多有用的功能,而这些功能在关系数据库中很难实现。

非关系型数据库领域中最著名的竞争者之一是 MongoDB。MongoDB 是一个开源项目,由位于纽约的 10gen 赞助(该公司打算通过许可和支持费用赚钱)。它用 C++ 编写,并且有适用于所有流行的现代库的驱动程序。该软件根据 Affero GNU 通用公共许可证获得许可,这意味着如果您修改了 MongoDB 源代码,并且这些修改可以在公共访问的网站上获得,则您必须将修改的源代码分发给他人。这与标准 GPL 不同,标准 GPL 不要求您公开与人们通过浏览器或其他 Internet 客户端交互的服务器端应用程序的源代码。

MongoDB 因其功能组合而获得了大量拥护者。它易于从各种语言中使用,速度极快(用 C++ 编写),受到公司和大型社区的积极支持,并且已被证明在许多情况下和高压条件下都是稳定的。它还包括许多用于索引和扩展的功能,使其具有吸引力。

MongoDB 与它的几个竞争对手一样,将自己描述为文档数据库。这并不意味着它是一个用于存储文档的文件系统,而是意味着它用“文档”的概念取代了表、行和列的模型,“文档”由一个或多个名称-值对组成。我发现将文档视为哈希表(或 Python 字典)更容易理解,其中键是字符串,值几乎可以是任何东西。这些文档中的每一个都存在于一个集合中,您可以拥有一个或多个集合。

在许多方面,您可以将 MongoDB 视为对象数据库,因为它允许您将项目作为对象存储和检索,而不是将它们强制放入二维表中。但是,此对象数据库仅存储基本对象类型——数字、字符串、列表和哈希,例如。幸运的是,这些类型可以灵活可靠地存储各种数据,因此这不是什么大问题。

下载和安装

要下载 MongoDB,请访问 mongodb.org,并检索适合您系统的版本。对于我的运行 Ubuntu 8.10 的服务器,我检索了 32 位版本的 MongoDB 1.2.2。有一个选项可以检索静态链接版本,但该站点本身表明这是一个后备方案,以防动态链接版本失败。

解压缩 MongoDB 服务器后,创建一个目录,用于存储其数据。默认情况下,这是 /data/db,您可以使用以下命令创建:

mkdir -p /data/db

使用以下命令启动 MongoDB 服务器进程:

./bin/mongod

现在您已经运行了一个服务器,您需要创建一个数据库。但是,此步骤是不必要的。如果您尝试连接到尚未定义的数据库,MongoDB 会为您创建它。我倾向于在 Ruby 中完成大部分 MongoDB 工作,所以我从 GitHub 下载并安装了 Ruby 的驱动程序,并启动了交互式 Ruby 解释器 irb。然后,我输入了

irb(main):001:0> require 'rubygems'
irb(main):002:0> require 'mongo'

加载 MongDB 驱动程序后,我能够连接到已经运行的服务器,创建了一个“atf”数据库

irb(main):005:0> db = Mongo::Connection.new.db("atf")

此后,db 是 Mongo::DB 类的实例,表示 MongoDB 数据库。每个数据库可以包含任意数量的集合,类似于关系数据库中的表。默认情况下,此示例数据库不包含任何集合,您可以通过以下小代码片段看到

irb(main):008:0> db.collection_names.each { |name| puts name }
=> [ ]

空列表的返回值表明数据库当前为空。

您可以通过在数据库连接上调用 collection 方法来创建一个新集合

irb(main):012:0> c = db.collection("stuff")

创建集合后,您还可以看到 MongoDB 静默地创建了第二个集合,名为 system.indexes,用于索引内容

irb(main):032:0> db.collection_names
=> ["stuff", "system.indexes"]

由于 MongoDB 是一个无模式数据库,您可以立即开始将项目存储到您的集合中,而无需定义其列或数据类型。实际上,这意味着您可以存储具有您选择的任何键和值的哈希。例如,您可以使用 insert 方法向您的集合添加一个新项目

irb(main):017:0> c.insert({:a => 1, :b => 2})
=> 4b6fe8983c1c7d6a6a000001

返回值是刚刚存储的此文档(或对象)的唯一 ID。您可以要求集合显示您已存储的内容,方法是调用其 find_one 方法

irb(main):021:0> c.find_one
=> {"_id"=>4b6fe8983c1c7d6a6a000001, "a"=>1, "b"=>2}

请注意,这里发生了两件事。首先,键已从 Ruby 符号转换为字符串。实际上,MongoDB 要求所有键都必须是字符串;由于符号在 Ruby 世界中被广泛用作哈希键,如果您使用它们,它们会被静默地转换为字符串。

其次,您可以看到另一个名为 _id 的键已添加到文档中,并且其值与您第一次插入时收到的返回值匹配。

您可以使用 count 方法询问集合它包含多少个文档

irb(main):026:0> c.count
=> 1

正如您可能期望的那样,您可以使用任意数量的不同语言来存储和检索数据。虽然您可能只使用一种语言,但 MongoDB(就像关系数据库一样)并不关心您使用哪种语言,并允许您自由混合和匹配它们。

在上面的示例中,我使用 Ruby 存储数据。我应该能够使用 Python 检索此数据,如下所示

>>> import pymongo
>>> from pymongo import Connection
>>> connection = Connection()
>>> db = connection.atf
>>> db.collection_names()
   [u'stuff', u'system.indexes']
>>> c = db.stuff

>>> c
   Collection(Database(Connection('localhost', 27017), u'atf'), 
 ↪u'stuff')

>>> c.find_one()
   {u'a': 1, u'_id': ObjectId('4b6fe8983c1c7d6a6a000001'), u'b': 2}

这里唯一的惊喜可能是字符串都存储为 Unicode,用u''Python 2.6(我在这里使用的版本)中的语法表示。此外,文档 ID(键为 _id)仍然存在,但它是一个对象,而不是一个字符串。

您还可以看到 MongoDB 开发人员已尽力保持不同语言之间的 API 相似。这意味着如果您使用多种语言,您可能会依赖相似(或相同)的方法名称来执行相同的任务。

查询

正如您所见,find_one 方法从集合中返回单个元素。类似的 find 方法使用 Enumerable 模块返回所有元素,允许您使用 each 迭代集合中的所有文档。例如,如果您添加另一个文档

irb(main):026:0> c.insert({'name' => 'Reuven',
                           'email_address' => 'reuven@lerner.co.il'})
=> 4b6ff0693c1c7d6ecd000001

您可以按如下方式检索 ID

irb(main):030:0> c.find.each {|i| puts i['_id']}
4b6fe8983c1c7d6a6a000001
4b6ff0693c1c7d6ecd000001

请注意,您可以通过将文档视为哈希来提取 _id 列。实际上,如果您要求 Ruby 显示对象的类而不是其 ID,则此怀疑会得到证实

irb(main):031:0> c.find.each {|i| puts i.class}
OrderedHash
OrderedHash

但是,也许您只对某些文档感兴趣。通过使用哈希调用 find,它将仅返回那些与您的哈希内容匹配的文档。例如

irb(main):040:0> c.find({'name' => 'Reuven'}).count
=> 1

如果没有任何内容与您传递的哈希匹配,您将获得一个空结果集

irb(main):041:0> c.find({'name' => 'Reuvennn'}).count
=> 0

您还可以搜索正则表达式

irb(main):042:0> c.find({'name' => /eu/}).count
=> 1

irb(main):043:0> c.find({'name' => /ez/}).count
=> 0

通过将哈希作为键的值传递,您还可以修改查询,传递定义 MongoDB 查询语法的参数。这些查询运算符都以美元符号 ($) 开头,并作为子哈希的键传递。例如,您可以检索“name”是指定数组中的值之一的所有文档,如下所示

irb(main):049:0> c.find({'name' =>
                           {'$in' => ['Reuven', 'Atara', 'Shikma',
 ↪'Amotz'] }  }
                       ).count
=> 1

您还可以通过使用类似的语法在结果集上调用 sort 方法来对结果进行排序

irb(main):049:0> c.find({'name' =>
                           {'$in' => ['Reuven', 'Atara', 'Shikma',
 ↪'Amotz'] }  }
                       ).sort({"name" => 1})

正如您可以对结果集进行排序一样,您还可以对其执行其他操作,这些操作类似于几个关系对应项,例如分组和限制结果数量。如果您习惯了函数式编程风格,在这种风格中,您可以将多个方法链接在一起,那么这种风格很容易适用于使用 MongoDB。

结论

MongoDB 因其高性能和易于学习的曲线而在开源和数据库世界中引起了许多涟漪。本月,我介绍了安装和使用 MongoDB 的基础知识。下个月,我将研究一些更高级的主题,例如索引(这使查询执行速度更快)、将对象相互嵌入以及跨集合引用对象。

资源

MongoDb 的主要站点,包括源代码和文档,位于 mongodb.org

MongoDB 的 Ruby 驱动程序托管在 GitHub 上:github.com/mongodb/mongo-ruby-driver。Python 驱动程序位于 github.com/mongodb/mongo-python-driver

有关 MongoDB 的出色介绍,包括有关 10gen 的一些公司背景以及如何在您的应用程序中使用它,请收听“FLOSS Weekly”播客的第 105 集。我发现该播客既有趣又信息丰富。

另一个好的介绍来自 John Nunemaker,他是 Ruby 世界中一位著名的博主:railstips.org/blog/archives/2009/06/03/what-if-a-key-value-store-mated-with-a-relational-database-system

一篇关于 MongDB 速度(相对于 CouchDB 和 MySQL)的博文位于 www.idiotsabound.com/did-i-mention-mongodb-is-fast-way-to-go-mongo

最后,我仍然认为动态语言和面向文档的数据库之间存在相似之处。Google 的工程师 Steve Yegge 写过动态语言日益普及的文章,我强烈推荐他的演示文稿,以获得对该问题有趣的视角:steve-yegge.blogspot.com/2008/05/dynamic-languages-strike-back.html

Reuven M. Lerner 是一位长期的 Web 开发人员、培训师和顾问。他是西北大学学习科学博士候选人。Reuven 与他的妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论