跳转到主要内容
Dan 提交于 8 December 2012

MongoDB的入门超级简单:在浏览器中打开try.mongodb.org即可。它提供一个教程,让你用该数据库来玩,而无需下载任何东西。如果你想在自己的电脑上使用,你可以从mongodb.org/downloads下载,并且可随时运行;没有复杂的配置文件要写。Drupal的整合项目在drupal.org/project/mongodb

MongoDB惊人地适合于Drupal 7,尽管很多Drupal 7的开发是先于MongoDB的。它是一个为Web而设计的数据库,所以它和全球最好的用来制作网站的软件Drupal匹配得如此之好也就不足为奇了。

MongoDB存储的基本上是JSON编码的文档(实际差别很小)。一个文档粗略等同于一条SQL记录。任意多个文档组成一个集合,粗略等同于一个SQL数据表。最后,数据库包含集合,和MySQL数据库包含数据表非常类似。

一个MySQL数据表只能有固定的记录,而一个MongoDB集合可以存储任何类型的文档,比如这样:

{ title: 'first document', length: 255 }, { name: 'John Doe', weight: 20 }

如此等等,任何东西都可以。没有CREATE TABLE命令,因为一个文档和另一个文档可以大不相同。

还记得在SQL中存储名字的问题吗?在这里不是问题了!

db.people.insert({ name: ['Juan', 'Carlos', 'Alfonso', 'Víctor', 'María', 'de', 'Borbón', 'y', 'Borbón-Dos', 'Sicilias'], title: 'King of Spain'})

如果你想要得到名叫Carlos的人的一个文档列表,你可以运行db.people.find({name: 'Carlos'})。它会找到西班牙国王,不管Carlos在名字列表的何处。

还有,属性可以比仅仅数字和字符串要多得多。

db.test.insert({ 'title': 'This is an example', 'body': 'This can be a very long string. The whole document is limited to a number of megabytes.', 'votes': 56,. 'options': { 'sticky': true, 'promoted': false, }, 'comments': [ { 'title': 'first comment', 'author': 'joe', 'published': true, }, { 'title': 'first comment', 'author': 'harry', 'homepage': 'http://example.com', 'published': false, } ] });

所以,文档有属性(上面例子中的title, body等等),并且属性可以有任何类型的值,如字符串(title)、数字(votes)、布尔值(options,sticky)、数组(comments)和更多对象(也叫作子文档,options就是一个例子)。文档能有多复杂是没有限制的。不过,文档有尺寸上的限制(曾经很长一段时间里是4MB;目前是16MB,但计划会增加到32MB);对于大部分网页来说,这个限制不是问题。仍就这个例子来说,一篇文章能有多少评论?几千条的空间是有的。如果不够,实际的正文(它从来不会被查询)可以单独存储。

因此同SQL相比,主要的优点是,无需事先指定数据表结构,其实没有固定的结构,值也可以是复杂的结构体。

针对上面的文档,还有一些更有趣的查找命令:

db.test.find({title: /^This/}); db.test.find({'options.sticky': true}); db.test.find({comments.author': 'joe'});

正如你所见,find命令使用的是同一个插入的JSON文档。第一个find命令使用正则表达式来查找标题以“This”打头的文章,第二个显示了在对象内部查找有多容易,第三个显示了对象数组也同样容易。对所有这三个查询进行索引也是可能的。

如果你看一看Drupal 7的带有字段的实体(Entity),它实际上是包含数组的对象。你可以把它们整个存储和索引到MongoDB里面。这正是最至关重要的:在SQL中,尽管你可以把相同结构的实体(比如相同类型的节点)存储在一个单独的数据表中,但如果你需要跨越这些类型进行查询(比如,显示一个用户收藏的所有近期内容,不管它们是文章还是照片),那么你就不能索引这个查询。记住,要快速执行一个这样的查询,你需要去规范化(denormalize);换句话说,在一张单独的表中保存一份你需要一起查询的数据的副本。这难于维护并且更新缓慢,因为每条数据都有很多副本。在MongoDB中,你可以轻松做到这个跨内容类型的查询,因为所有节点都可以待在一个单独的表中,你只要在‘favorite’和‘created’上建立一个索引就万事大吉了。

总而言之,SQL是不能存储复杂的结构体的。尽管MongoDB不是针对该问题的唯一解决方案,但相比SQL使用多表和去规范化的方法,它实现和维护起来都要简单得多。这个问题在Drupal 6中使用CCK时就存在,而随着Drupal 7中实体和字段API的引入,它已经变成了一个重要的问题。

要使用MongoDB作为默认的字段存储引擎,把以下代码:

$conf['field_storage_default'] = 'mongodb_field_storage';

加到(sites/default或相应的sites目录下的)settings.php文件中。如果你是从代码创建字段,设置字段数组的‘storage’键同样可行。用户界面上不提供该选择,所以如果你完全使用用户界面,就不能按字段来选择存储方式。

尽管MongoDB最好的方面可能是作为一个极佳的存储引擎而存在,可它甚至还不止于此。它还能解决其它问题。再次重申,它并非唯一的解决方案,但确是非常优秀的一个。

首先,让我们来看一些与写入有关的更多特性。比如像这样来增加我们的测试文档中的投票数:

o = db.test.findOne({nid: 12345678}); o.votes++; db.test.save(o);

这个代码尽管工作,却不怎么漂亮,而且容易导致争用情况(race condition)。争用情况是开发者的灾星,因为它极难复制却会产生神秘的错误。让我们来看一个例子。下面是当两个用户试图投票时可能会依次发生的事:

  • 用户A运行o = db.test.findOne({nid: 12345678});-- 票数为56。
  • 用户A运行o.votes++
  • 用户B运行o = db.test.findOne({nid: 12345678});-- 票数为56。
  • 用户A运行db.test.save(o);把票数设置为57。
  • 用户B运行o.votes++
  • 用户B运行db.test.save(o);把票数设置为57。

在MongoDB中,你可以替换成:

db.test.update({nid: 12345678}, {$inc : { votes : 1 }});

  • 用户A运行该命令,票数加1,为57
  • 用户B运行该命令,票数加1,为58

这是一个原子操作,不可能发生由于争用情况而产生的错误了。注意更新操作同样是JSON文档的形式;有特殊的更新操作符,如上面所示的$inc,但语法还是一样的。

你也可以指定一个多文档更新

db.test.update({nid: {'$in': [123, 456]}, {$inc : { votes : 1 }}, {multiple:1});

尽管单个文档会原子地递增(无争用情况),多文档更新却不是原子的。我在前面列出事务属性的时候提到过,在事务的情况中,原子性意味着,要么所有文档更新,要么没有文档更新。在这就不是这种情况了,因为MongoDB中没有事务:有可能一个文档更新失败了,却对其它成功更新的文档没有任何影响。MongoDB还缺少事务所需的另一项性能,叫作隔离性:当一个文档已经对每个客户更新了,其它文档可能还拥有它们的旧数据。

按文档原子递增使得MongoDB成为存储各种统计数据的理想选择。例如,你可能需要存储并显示一个节点被查看的次数。首先,给节点添加一个数字字段,叫作‘views’,接下来,你会看到一小段代码,接收节点ID作为参数,并把views字段的值增加1。注意,为了阻止浏览器或代理缓存它,这段代码发送一个和Drupal一样的头部,最后显示一个1x1的透明GIF。

<?php if (!empty($_GET['nid']) && $_GET['nid'] == (int) $_GET['nid']) { define('DRUPAL_ROOT', getcwd()); require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); require_once DRUPAL_ROOT . '/sites/all/modules/mongodb/mongodb.module'; $find = array('_id' => (int) $_GET['nid']); $update = array('$inc' => array('views.value' => 1)); mongodb_collection('fields_current', 'node')->update($find, $update); } header('Content-type: image/gif'); header('Content-length: 43'); header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); header("Expires: Sun, 19 Nov 1978 05:00:00 GMT"); header("Cache-Control: must-revalidate"); printf('%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c ', 71, 73, 70, 56, 57, 97, 1, 0, 1, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 33, 249, 4, 1, 0, 0, 0, 0, 44, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 68, 1, 0, 59); ?>

现在你要做的就是把它保存为Drupal根目录下的stats.php,并在node.tpl.php中加入一个nid; ?>"/>。通过这种方式来统计节点的查看次数,即使页面是缓存或者Varnish提供的,查看也会被统计。

这会扼杀站点的性能吗?要视情况而定。它确实造成每次有人加载页面都动用到PHP,这可能是可接受的,也可能是不可接受的。对于大多数站点来说,没问题。如果站点用它来工作,MongoDB不会产生问题。鉴于MongoDB是在内存中操作数据库,偶尔写回磁盘,因为写入仅仅发生在内存中,所以要快很多。同时,它尽力在原地更新数据,所以索引无需做没必要的更新。

2011年的新特性是即使使用单个服务器时也不丢失写入的功能。对MongoDB最大的抱怨之一是,因为它只是偶尔才写回数据,所以如果服务器崩溃,数据可能会丢失。以前,通过使应用程序等待写入被复制到另一台服务器,寄望于两台服务器不会同时崩溃,这种情况得以缓解。但是现在,如果mongod是以-dur选项启动,则写入不丢失。