2. 基础

从了解基本的构成开始,我们开始踏上MongoDB探索之路。显然,这是认识MongoDB的关键,同时也有助于搞清楚MongoDB适用范围的高层次问题。

作为开始,我们需要了解6个简单的概念:

  1. MongoDB有着与您熟知的‘数据库’(database,对于Oracle就是‘schema’)一样的概念。在一个MongoDB的实例中您有若干个数据库或者一个也没有,不过这里的每一个数据库都是高层次的容器,用来储存其他的所有数据。
  2. 一个数据库可以有若干‘集合’(collection),或者一个也没有。集合和传统概念中的‘表’有着足够多的共同点,所以您大可认为这两者是一样的东西。
  3. 集合由若干‘文档’(document)组成,也可以为空。类似的,可以认为这里的文档就是‘行’。
  4. 文档又由一个或者更多个‘域’(field)组成,您猜的没错,域就像是‘列’。
  5. ‘索引’(index)在MongoDB中的意义就如同索引在RDBMS中一样。
  6. ‘游标’(cursor)和以上5个概念不同,它很重要但是却常常被忽略,有鉴于此我认为应该进行专门讨论。关于游标有一点很重要,就是每当向MongoDB索要数据时,它总是返回一个游标。基于游标我们可以作诸如计数或是直接跳过之类的操作,而不需要真正去读数据。

小结一下,MongoDB由‘数据库’组成,数据库由‘集合’组成,集合由‘文档’组成。‘域’组成了文档,集合可以被‘索引’,从而提高了查找和排序的性能。最后,我们从MongoDB读取数据的时候是通过‘游标’进行的,除非需要,游标不会真正去作读的操作。

您也许已经觉得奇怪,为什么要用新的术语(表换成集合,行换成文档,列换成域),这不是越弄越复杂了么?这是因为虽然这些概念和那些关系数据库中的相应概念很相似,但是还是存在差异的。关键的差异在于关系数据库是在‘表’这一层次定义‘列’的,而一个面向文档的数据库则是在‘文档’这一层次定义‘域’的。也就是说,集合中的每个文档都可以有独立的域。因此,虽说集合相对于表来说是一个简化了的容器,而文档则包含了比行要多得多的信息。

虽然这些异同很重要,但是如果您现在还没搞清楚也不必担心。以后试着插入几次(数据)就知道我们这里说的是什么了。最后,集合对其中储存的内容并没有严格的要求(它是无模式的(schema-less)),域是与其所在的文档绑定的。当中的优缺点我们会在后续的章节中继续探讨。

开始动手实践吧。如果您还没有运行Mongo,现在就可以启动mongod服务器以及Mongo的shell。Mongo的shell运行在JavaScript之上,您可以执行一些全局的指令,如help或者exit。操作对象db来执行针对当前数据库的操作,例如db.help()或是db.stats()。操作对象db.COLLECTION_NAME来执行针对某一给集合的操作,比如说db.unicorns.help()或是db.unicorns.count()。我们以后将会有许多针对集合的操作。

试试输入db.help(),您会看到一串命令列表,这些命令都可以用来操作db对象。

顺带说一句,因为我们用的是JavaScript的shell,如果您执行一个命令而忘了加上(),您看到的将是这个命令的实现而并没有执行它。知道这个,您在这么做并看到以function(...){开头的信息的时候就不会觉得惊讶了。比如说如果您输入db.help(后面没有括弧),你就将看到help命令的具体实现。

首先我们用全局命令use来切换数据库。输入use learn。这个数据库是否存在并没有关系,我们创建第一个集合后这个learn数据库就会生成的。现在您应该已经在一个数据库里面了,可以执行一些诸如db.getCollectionNames()的数据库命令了。如果您现在就这么做,将会看到一个空的数组([])。因为集合是无模式的,我们不需要专门去创建它。我们要做的只是把一个文档插入一个新的集合,这个集合就生成了。您可以像下面一样调用insert命令去插入一个文档:

db.unicorns.insert({name: 'Aurora', gender: 'f', weight: 450})

以上命令对unicorns对象执行insert操作,并传入一个参数。在MongoDB内部,数据是以二进制的串行JSON格式存储的。这对外部的用户而言,意味着JSON的大量应用,就如同上面的参数一样。如果我们现在执行db.getCollectionNames(),将看到两个集合:unicorns以及system.indexessystem.indexes在每个数据库中都会创建,它包含了数据库中的索引信息。

现在您可以对unicorns对象执行find命令,得到一个文档列表:

db.unicorns.find()

请注意,除了您在文档中输入的各个域,还有一个一个叫做_id的域。每一个文档都必须有一个独立的_id域。您可以自己创建,也可以让MongoDB为您生成这个ObjectId。大部分情况下您还是会让MongoDB为您生成ID的。默认情况下,_id域是被索引了的——这就是为什么会有system.indexes创建出来的原因。看看system.indexes有什么:

db.system.indexes.find()

在结果中您会看到该索引的名字,它所绑定的数据库和集合的名字,还有包含这个索引的域。

回到我们前面关于无模式集合的讨论。现在往unicorns插入一个完全不同的文档,比如说这样:

db.unicorns.insert({name: 'Leto', gender: 'm', home: 'Arrakeen', worm: false})

再次用find可以列出所有的文档。学习到后面,我们将继续讨论MongoDB无模式的这一有趣的行为,不过我希望您已经开始慢慢了解为什么传统的那些术语不适合用在这里了。

掌握选择器(selector)

除了刚才介绍过的6个概念,MongoDB还有一个很实用的概念:查询选择器(query selector),在进入更高阶的内容之前,您也需要很好的了解它是什么。 MongoDB的查询选择器就像SQL代码中的where语句。因此您可以用它在集合中查找,统计,更新或是删除文档。选择器就是一个JSON对象,最简单的形式就是{},用来匹配所有的文档(null也可以)。如果我们需要找到所有雌性的独角兽(unicorn),我们可以用选择器{gender:'f'}来匹配。

在深入选择器之前,我们先输入一些数据以备后用。首先用db.unicorns.remove()删除之前我们在unicorns集合中输入的所有数据。(由于在这条命令中我们没有指定选择器,于是所有的文档都将被清除)。然后用下面的插入命令准备一些数据(建议拷贝粘帖这些命令):

db.unicorns.insert({name: 'Horny', dob: new Date(1992,2,13,7,47), loves: ['carrot','papaya'], weight: 600, gender: 'm', vampires: 63});
db.unicorns.insert({name: 'Aurora', dob: new Date(1991, 0, 24, 13, 0), loves: ['carrot', 'grape'], weight: 450, gender: 'f', vampires: 43});
db.unicorns.insert({name: 'Unicrom', dob: new Date(1973, 1, 9, 22, 10), loves: ['energon', 'redbull'], weight: 984, gender: 'm', vampires: 182});
db.unicorns.insert({name: 'Roooooodles', dob: new Date(1979, 7, 18, 18, 44), loves: ['apple'], weight: 575, gender: 'm', vampires: 99});
db.unicorns.insert({name: 'Solnara', dob: new Date(1985, 6, 4, 2, 1), loves:['apple', 'carrot', 'chocolate'], weight:550, gender:'f', vampires:80});
db.unicorns.insert({name:'Ayna', dob: new Date(1998, 2, 7, 8, 30), loves: ['strawberry', 'lemon'], weight: 733, gender: 'f', vampires: 40});
db.unicorns.insert({name:'Kenny', dob: new Date(1997, 6, 1, 10, 42), loves: ['grape', 'lemon'], weight: 690,  gender: 'm', vampires: 39});
db.unicorns.insert({name: 'Raleigh', dob: new Date(2005, 4, 3, 0, 57), loves: ['apple', 'sugar'], weight: 421, gender: 'm', vampires: 2});
db.unicorns.insert({name: 'Leia', dob: new Date(2001, 9, 8, 14, 53), loves: ['apple', 'watermelon'], weight: 601, gender: 'f', vampires: 33});
db.unicorns.insert({name: 'Pilot', dob: new Date(1997, 2, 1, 5, 3), loves: ['apple', 'watermelon'], weight: 650, gender: 'm', vampires: 54});
db.unicorns.insert({name: 'Nimue', dob: new Date(1999, 11, 20, 16, 15), loves: ['grape', 'carrot'], weight: 540, gender: 'f'});
db.unicorns.insert({name: 'Dunx', dob: new Date(1976, 6, 18, 18, 18), loves: ['grape', 'watermelon'], weight: 704, gender: 'm', vampires: 165});

现在我们有足够的数据,我们可以来掌握选择器了。{field: value}用来查找所有field等于value的文档。通过{field1: value1, field2: value2}的形式可以实现操作。$lt$lte$gt$gte以及$ne分别表示小于、小于或等于、大于、大于或等于以及不等于。举个例子,查找所有体重超过700磅的雄性独角兽的命令是:

db.unicorns.find({gender: 'm', weight: {$gt: 700}})
//或者 (效果并不完全一样,仅用来为了演示不同的方法)
db.unicorns.find({gender: {$ne: 'f'}, weight: {$gte: 701}})

$exists操作符用于匹配一个域是否存在,比如下面的命令:

db.unicorns.find({vampires: {$exists: false}})

会返回单个文档(译者:只有这个文档没有vampires域)。如果需要 而不是 ,可以用$or操作符并作用于需要进行或操作的数组:

db.unicorns.find({gender: 'f', $or: [{loves: 'apple'}, {loves: 'orange'}, {weight: {$lt: 500}}]})

以上命令返回所有或者喜欢苹果,或者喜欢橙子,或者体重小于500磅的雌性独角兽。

您可能已经注意到了,在最后的一个例子中有一个非常棒的特性:loves域是一个数组。在MongoDB中数组是一级对象(first class object)。这是非常非常有用的功能。一旦用过,没有了它你可能都不知道怎么活下去。更有意思的是基于数组的选择是非常简单的:{loves: 'watermelon'}就会找到loves中有watermelon这个值的所有文档。 除了我们目前所介绍过的,还有更多的操作符 可以使用。最灵活的是$where,允许输入JavaScript并在服务器端运行。这些都在MongoDB网站的Advanced Queries部分有详细介绍。不过这里介绍的都是基本的命令,了解了这些您就可以开始使用Mongo了。而这些命令也往往是您大多数时间会用到的所有命令。

我们已经介绍过怎样在find命令中使用选择器了。此外选择器还可以用在remove命令中,我们已经大致提过;还有count命令中,我们并没有介绍不过您自己可以去试试看;还有update命令,我们在后面还会提到。

MongoDB为_id域生成的ObjectId也是可以被选择的,就像这样:

db.unicorns.find({_id: ObjectId("TheObjectId")})

本章小结

我们还没有介绍update命令以及find的更华丽的功能。不过我们已经让MongoDB运行起来,并且执行了insertremove命令(这些命令看过本章的介绍也差不多了)。我们还介绍了find命令并见识了什么是MongoDB的选择器。一个好的开头为后面的深入奠定了坚实的基础。不管你信不信,您事实上已经了解了关于MongoDB所需要知道的知识——它本来就是易学易用的。我强烈建议您在继续读下去之前多多在MongoDB练习尝试一下。试着插入不同的文档,可以插入到新的集合中,并且熟悉不同的选择器表达式。多用findcountremove。经过您自己的实践,那些初看很别扭的东西最后都会变得好用起来的。