铸造车间 - 高级 MongoDB

作者:Reuven M. Lerner

上个月,我开始讨论 MongoDB,这是一种开源的非关系型“文档型”数据库,在过去一年中越来越受欢迎。与将所有信息存储在二维表中的关系数据库不同,MongoDB 将所有内容存储在类似于哈希表集合的东西中。

在关系数据库中,您可以确定表中的每个记录(即行)都具有相同的列数和列集。相比之下,MongoDB 是无模式的,这意味着它不对列强制执行此类规则。MongoDB 集合中的两条记录可能具有相同的键,也可能没有两个共同的键。确保键有意义,并且不会容易被滥用或出错,是程序员的责任。

事实证明,使用 MongoDB 非常简单,正如我上个月在几个示例中展示的那样。一旦您设置了数据库和集合,您就可以使用您喜欢的语言中的对象和 MongoDB 查询语言的组合来添加、删除和修改记录。

然而,易于使用 MongoDB 并不意味着它缺乏强大的功能。本月,我将介绍一些您在将 MongoDB 融入您的应用程序时可能会使用的功能,例如索引和对象关系。如果您像我一样,您会发现有很多值得喜欢的地方;此外,使用 MongoDB 会促使您以新的和不同的方式思考您的数据。

索引

正如我上个月解释的那样,MongoDB 有自己的查询语言,允许您检索属性与某些条件匹配的记录。例如,如果您有一个图书数据库,您可能想要查找所有具有特定书名的图书。执行此类检索的一种方法是迭代每个记录,并提取所有与所讨论的书名完全匹配的记录。在 Ruby 中,您可以将其表示为

books.find_all {|b| b.title == search_title}

这种方法的问题在于它非常慢。系统需要迭代每个项目,这意味着随着图书列表的增长,查找您要查找的内容所需的时间也会增加。

数据库程序员长期以来都知道,解决这个问题的方法是使用索引。索引有多种形式,但基本思想是它们允许您立即找到标题具有特定值的所有记录(或任何列字段),而无需扫描每个单独的记录。因此,MongoDB 支持索引也就不足为奇了。您如何使用它们?

继续以本书示例为例,我将大约 43,000 本书插入到 MongoDB 集合中。每个插入的文档都是一个 Ruby 哈希,存储了图书的 ISBN、书名、重量和出版日期。然后,我可以使用 MongoDB 的客户端程序检索一本书,该程序提供了一个交互式 JavaScript 界面

   ./bin/mongo atf
> db.books.count()
   38202
> db.books.find({isbn:'9789810185060'})
   { "_id" : ObjectId("4b8fca3ef23f3c614600a8c2"),
     "title" : "Primary Mathematics 4A Textbook",
     "weight" : 40,
     "publication_date" : "2003-01-01",
     "isbn" : "9789810185060" }

查询似乎执行得足够快,但是如果存在数百万条记录,它会慢得多。您可以通过在 isbn 列上添加索引来提高数据库服务器的速度

> db.books.ensureIndex({isbn:1})

这将在 isbn 列上以升序创建索引。您也可以指定 -1(而不是 1)来指示项目应按降序索引。

正如关系数据库自动在表的“主键”列上放置索引一样,MongoDB 自动索引集合上唯一的 _id 属性。每个其他索引都需要手动创建。实际上,现在如果您获取索引列表,您将看到不仅 isbn 列被索引,而且 _id 也被索引

> db.books.getIndexes()
   [
       {
               "name" : "_id_",
               "ns" : "atf.books",
               "key" : {
                       "_id" : ObjectId("000000000000000000000000")
               }
       },
       {
               "ns" : "atf.books",
               "key" : {
                       "isbn" : 1
               },
               "name" : "isbn_1"
       }
   ]

现在您可以执行与之前相同的查询,请求所有具有特定 ISBN 的图书。您不会看到结果集有任何变化;但是,您应该比以前更快地获得响应。

您还可以创建复合索引,它查看多个键

> db.books.ensureIndex({title:1, weight:1})

将书名的索引与其重量的索引结合起来可能没有意义。然而,这正是我现在在示例中所做的。如果您稍后决定不需要此索引,您可以使用以下命令将其删除

> db.books.dropIndex('title_1_weight_1')
   { "nIndexesWas" : 3, "ok" : 1 }

因为我使用的是 JavaScript 界面,所以响应是一个 JSON 对象,指示以前有三个索引(现在只有两个),并且该函数已成功执行。如果您第二次尝试删除索引,您将收到一条错误消息

> db.books.dropIndex('title_1_weight_1')
   { "errmsg" : "index not found", "ok" : 0 }
强制唯一性

索引不仅可以加快许多查询的速度,还可以让您确保唯一性。也就是说,如果您想确保特定属性在集合中的所有文档中都是唯一的,则可以使用“unique”参数定义索引。

例如,让我们从当前集合中获取一条记录

> db.books.findOne()
   {
      "_id" : ObjectId("4b8fc9baf23f3c6146000b90"),
      "title" : "\"Gateways to Academic Writing: Effective Sentences,
                   Paragraphs, and Essays\"",
      "weight" : 0,
      "publication_date" : "2004-02-01",
      "isbn" : "0131408887"
   }

如果您尝试插入具有相同 ISBN 的新文档,MongoDB 不会在意

> db.books.save({isbn:'0131408887', title:'fake book'})

但从理论上讲,每本 ISBN 应该只有一本书。这意味着数据库可以(并且应该)对 ISBN 具有唯一性约束。您可以通过删除并重新创建索引来实现此目的,指示新版本的索引也应强制执行唯一性

> db.books.dropIndex("isbn_1")
   { "nIndexesWas" : 2, "ok" : 1 }
> db.books.ensureIndex({isbn:1}, {unique:true})
   E11000 duplicate key errorindex: atf.books.$isbn_1  
   ↪dup key: { : "0131408887" }

糟糕。事实证明,数据库中已经存在一些重复的 ISBN。好消息是 MongoDB 显示了哪个键是罪魁祸首。因此,您可以浏览数据库(手动或自动,取决于数据集的大小)并删除此键,重新尝试创建索引,依此类推,直到一切正常。或者,您可以告诉 ensureIndex 函数它应该删除任何重复的记录。

是的,您没看错。MongoDB 将在您要求的情况下,不仅创建唯一索引,还会删除任何可能导致该约束被违反的内容。我非常确定我不会在实际生产数据上使用此功能,仅仅是因为想到我的数据库会删除数据就让我感到害怕。但在本示例中,对于玩具数据集,它工作得很好

> db.books.ensureIndex({isbn:1}, {unique:true, dropDups:true})
   E11000 duplicate key errorindex: atf.books.$isbn_1  
   ↪dup key: { : "0131408887" }

现在,如果您再次尝试插入非唯一 ISBN 会发生什么?

> db.books.save({isbn:'0131408887', title:'fake book'})
   E11000 duplicate key errorindex: atf.books.$isbn_1  
   ↪dup key: { : "0131408887" }

您可以在集合上拥有任意数量的索引。与关系数据库一样,索引的主要成本在您插入或更新数据时很明显,因此如果您希望大量插入或更新文档,您应该仔细考虑要创建多少索引。

第二个,也是更微妙的问题(在 David Mytton 的博客文章中引用——请参阅资源)是,每个 MongoDB 数据库中都有一个命名空间限制,并且此命名空间由集合和索引共同使用。

组合对象

对象数据库(或 MongoDB 将自己描述为“文档”数据库)的众多优点之一是,您可以将几乎任何东西存储在其中,而不会出现将对象存储在关系数据库的二维表中时存在的“阻抗失配”问题。因此,如果您的对象包含一些字符串、一些日期和一些整数,您应该没问题。

但是,在许多情况下,这还远远不够。一个经典的例子(在许多 MongoDB 常见问题解答和访谈中讨论过)是博客。拥有博客文章的集合是有意义的,并且每篇文章都有一个日期、标题和正文。但是,您还需要作者,并且假设您想要存储的不仅仅是作者姓名或其他简单的文本字符串,您可能需要将每个作者存储为对象。

那么,您如何做到这一点呢?最简单的方法是将对象与每篇博文一起存储。如果您以前使用过 Ruby 或 Python 等高级语言,这不会让您感到惊讶;您只是将哈希粘贴到哈希中(或者如果您是 Python 黑客,则将字典粘贴到字典中)。因此,在 JavaScript 客户端中,您可以说

> db.blogposts.save({title:'title',
                        body:'this is the body',
                        author:{name:'Reuven', 
                        ↪email:'reuven@lerner.co.il'} })

请记住,如果 MongoDB 尚不存在集合,它会为您创建一个集合。然后,您可以使用以下命令检索您的帖子

> db.blogposts.findOne()
   {
           "_id" : ObjectId("4b91070a9640ce564dbe5a35"),
           "title" : "title",
           "body" : "this is the body",
           "author" : {
                   "name" : "Reuven",
                   "email" : "reuven@lerner.co.il"
           }
   }

或者,您可以使用以下命令检索该作者的电子邮件地址

> db.blogposts.findOne()['author']['email']
   reuven@lerner.co.il

或者,您甚至可以搜索

> db.blogposts.findOne({title:'titleee'})
   null

换句话说,没有帖子与搜索条件匹配。

现在,如果您使用关系数据库有一段时间了,您可能会想,“等一下。他是说我应该将相同的作者对象与作者制作的每篇文章一起存储吗?” 答案是肯定的——我承认这让我感到毛骨悚然。MongoDB 与许多其他文档数据库一样,不要求甚至不期望您规范化您的数据——这与您使用关系数据库所做的相反。

非规范化方法的优点是它通常易于使用且速度更快。正如每个学习过规范化的人都知道的那样,缺点是如果您需要更新作者的电子邮件地址,您需要迭代集合中的所有条目——在许多情况下,这是一项昂贵的任务。此外,总是存在不同博客文章以不同方式拼写同一作者姓名的可能性,从而导致数据完整性问题。

如果说在使用 MongoDB 时有一个问题让我感到犹豫,那就是这个问题——数据未规范化与我多年来所做的一切背道而驰。我不确定我的反应是否表明我需要放松对此问题的态度,仅针对特别合适的任务选择 MongoDB,或者我是否是恐龙。

MongoDB 确实提供了一种部分解决方案。您可以输入对另一个对象的引用,而不是将对象嵌入到另一个对象中,无论是在同一集合中还是在另一个集合中。例如,您可以在数据库中创建一个新的“authors”集合,然后创建一个新的作者

> db.authors.save({name:'Reuven', email:'reuven@lerner.co.il'})

> a = db.authors.findOne()
   {
           "_id" : ObjectId("4b910a469640ce564dbe5a36"),
           "name" : "Reuven",
           "email" : "reuven@lerner.co.il"
   }

现在您可以将此作者分配给您的博客文章,替换之前的对象文字

> p = db.blogposts.findOne()
> p['author'] = a

> p
   {
           "_id" : ObjectId("4b91070a9640ce564dbe5a35"),
           "title" : "title",
           "body" : "this is the body",
           "author" : {
                   "_id" : ObjectId("4b910a469640ce564dbe5a36"),
                   "name" : "Reuven",
                   "email" : "reuven@lerner.co.il"
           }
   }

尽管博客文章看起来与您之前拥有的文章相似,但请注意它现在有自己的“_id”属性。这表明您正在引用 MongoDB 中的另一个对象。对该对象的更改会立即反映出来,您可以在此处看到

> a['name'] = 'Reuven Lerner'
   Reuven Lerner
> p
   {
           "_id" : ObjectId("4b91070a9640ce564dbe5a35"),
           "title" : "title",
           "body" : "this is the body",
           "author" : {
                   "_id" : ObjectId("4b910a469640ce564dbe5a36"),
                   "name" : "Reuven Lerner",
                   "email" : "reuven@lerner.co.il"
           }
   }

看到作者的“name”属性是如何立即更新的吗?那是因为您在这里有一个对象引用,而不是一个嵌入式对象。

鉴于您可以轻松地从其他对象引用对象,为什么不一直这样做呢?老实说,这绝对是我的偏好,也许反映了我多年使用关系数据库的经验。相比之下,MongoDB 的作者表示,这种方法的主要问题是它需要从数据库进行额外的读取,这会减慢数据检索过程。您将不得不决定哪些权衡对您现在和未来的需求是合适的。

结论

MongoDB 是一个令人印象深刻的数据库,拥有广泛的文档和驱动程序。开始使用 MongoDB 非常容易,即使对于只有一点 JavaScript 和数据库经验的人来说,交互式 shell 也非常简单明了。索引非常容易理解、创建和应用。

事情变得棘手甚至棘手的地方恰恰是在关系数据库几十年来一直擅长(并且经过优化)的领域——即,相关对象之间的交互和关联,在不牺牲太多速度的情况下确保数据完整性。我相信 MongoDB 将继续在这方面改进,但就目前而言,这是 MongoDB 最让我困扰的事情。尽管如此,我对迄今为止所看到的一切印象深刻,我可以很容易地想象在未来的一些项目中使用它,特别是那些跨集合引用数量有限的项目。

资源

MongoDB 的主站点,包括源代码和文档,位于 mongodb.org。交互式、基于 JavaScript 的 shell 的参考指南位于 www.mongodb.org/display/DOCS/dbshell+Reference

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

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

Mathias Meyer 在他的博客上对 MongoDB 进行了出色的介绍和描述:www.paperplanes.de/2010/2/25/notes_on_mongodb.html

由于 MongoDB 是一个“文档”数据库,您可能想知道是否可以生成文档的全文索引。答案是“有点”,更多信息和提示请访问 www.mongodb.org/display/DOCS/Full+Text+Search+in+Mongo

最后,David Mytton 最近撰写了一篇博文,其中描述了他在生产环境中使用 MongoDB 时遇到的一些问题:blog.boxedice.com/2010/02/28/notes-from-a-production-mongodb-deployment

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

加载 Disqus 评论