孜孜不倦的程序员

通过 MongoDB 使用 NoSQL

Ted Neward

下载代码示例

自从 2000 年宣布 Microsoft .NET Framework 并在 2002 年首次发行以来的过去近十年中,.NET 开发人员一直努力适应 Microsoft 推出的各种新事物。但这似乎还不够,“社区”(包含所有开发人员,无论他们是否每天都使用 .NET)也开始行动,创造出更多的新事物来填补 Microsoft 未覆盖到的空白,对您而言这可能是制造混乱和干扰。

在 Microsoft 的支持 范围之外,该社区所酝酿出的“新”事物之一就是 NoSQL 运动,一组开发人员公开质疑将所有数据存储于某种形式的关系数据库系统的这种观念。表、行、列、主键、外键约束、关于 null 的争论以及有关主键是否应该为自然键或非自然键的辩论……还有什么是神圣不可侵犯的?

在本文及其后续文章中,我将探讨 NoSQL 运动所倡导的主要工具之一:MongoDB,根据 MongoDB 网站的陈述,该工具的名称源自于“humongous”(并不是我杜撰的)。我基本上会讨论到与 MongoDB 相关的方方面面:安装、浏览以及在 .NET Framework 中使用 MongoDB。其中包括其提供的 LINQ 支持;在其他环境(桌面应用程序和 Web 应用程序及服务)中使用 MongoDB;以及如何设置 MongoDB,以免 Windows 生产管理员向您提出严重抗议。

问题(或者,为何我要再次关注?)

在深入了解 MongoDB 之前,读者自然要问为什么 .NET Framework 开发人员应该牺牲接下来宝贵的大约半小时时间继续待在电脑前阅读本文。毕竟,SQL Server 有免费、可再发行的 Express Edition,提供比企业或数据中心绑定的传统关系数据库更精简的数据存储方案,而且毫无疑问,还可以使用大量工具和库来轻松访问 SQL Server 数据库,其中包括 Microsoft 自己的 LINQ 和实体框架。

但问题在于,关系模型(指关系模型本身)的优点也是其最大的缺点。大多数开发人员(无论是 .NET、Java 还是其他开发人员都在此列)在经历短短几年的开发工作之后,就会一一痛诉这种表/行/列的“方正”模型如何不能令其满意。尝试对分层数据进行建模的举动甚至能让最有经验的开发人员完全精神崩溃,类似情况不甚枚举,因此 Joe Celko 还写过一本书《SQL for Smarties, Third Edition》(Morgan-Kaufmann,2005),其中完全是关于在关系模型中对分层数据建模的概念。如果在此基础之上再增加一个基本前提:关系数据库认为数据的结构(数据库架构)不灵活,则尝试支持数据的临时“添加”功能将变得十分困难。(快速回答下面的问题:你们之中有多少人处理过包含一个 Notes 列(乃至 Note1、Note2、Note3……)的数据库?)

NoSQL 运动中没有任何人会说关系模型没有优点,也没有人会说关系数据库将会消失,但过去二十年开发人员生涯的一个最基本的事实是,开发人员经常将数据存储到本质上并非关系模型(有时甚至与这种模型相去甚远)的关系数据库中。

面向文档的数据库便是用于存储“文档”(这是一些紧密结合的数据集合,通常并未关联到系统中的其他数据元素),而非“关系”。例如,博客系统中的博客条目彼此毫无关联,即使出现某一篇博客确实引用到另一篇博客的情况,最常用的关联方法也是通过超链接(旨在由用户浏览器解除引用),而非内部关联。对本博客条目的评论完全局限于本博客条目的内部范围,不管评论的是什么博客条目,极少有用户想查看包含所有评论的内容集合。

此外,面向文档的数据库往往在高性能或高并发性环境中表现突出:MongoDB 专门迎合高性能需求,而它的近亲 CouchDB 则更多的是针对高并发性的情况。两者都放弃了对多对象事务的支持,也就是说,尽管它们支持在数据库中对单个对象进行的并发修改,但若尝试一次性对多个对象进行修改,将会在一小段时间内看到这些修改正依序进行。文档以“原子方式”更新,但不存在涉及多文档更新的事务概念。这并不意味着 MongoDB 没有任何稳定性,只是说 MongoDB 实例与 SQL Server 实例一样不能经受电源故障。需要原子性、一致性、隔离性和持久性 (ACID) 完整要素的系统更适合采用传统的关系数据库系统,因此关键任务数据很可能不会太快出现在 MongoDB 实例内,但 Web 服务器上的复制数据或缓存数据可能要除外。

一般来说,若应用程序及组件需要存储可快速访问且常用的数据,则采用 MongoDB 可以取得较好效果。网站分析、用户首选项和设置(以及包含非完全结构化数据或需采用结构灵活的数据的任何系统类型)都是采用 MongoDB 的自然之选。这并不意味着 MongoDB 不能作为操作型数据的主要数据存储库;只是说 MongoDB 能在传统 RDBMS 所不擅长的领域内如鱼得水,另外它也能在大量其他适合的领域内大展拳脚。

入门

前面提到过,MongoDB 是一款开源软件包,可通过 MongoDB 网站 mongodb.com 轻松下载。在浏览器中打开该网站应该就能找到 Windows 可下载二进制包的链接,请在页面右侧查找“Downloads”链接。另外,如果更愿意使用直接链接,请访问 mongodb.org/display/DOCS/Downloads。截至本文撰写之时,其稳定版本为发行版 1.2.4。它其实就是一个 .zip 文件包,因此相对而言,其安装过程简单得可笑:只需在任何想要的位置解压 zip 包的内容。

没开玩笑,就是这样。

该 .zip 文件解压后生成三个目录:bin、include 和 lib。唯一有意义的目录是 bin 目录,其中包含八个可执行文件。除此之外不再需要任何其他的二进制(或运行时)依赖文件,而事实上,现在只需关注其中的两个可执行文件。这两个文件分别是 mongod.exe(即 MongoDB 数据库进程本身)和 mongo.exe(即命令行 Shell 客户端,其使用方法通常类似于传统的 isql.exe SQL Server 命令行 Shell 客户端,用于确保所有内容都已正确安装且能正常运行,并用于直接浏览数据、执行管理任务)。

验证所有内容是否正确安装的过程十分简单,只需在命令行客户端上启动 mongod。默认情况下,MongoDB 将数据存储在默认的文件系统路径 c:\data\db,但该路径是可以配置的,方法是在命令行上通过 --config 命令按名称传递一个文本文件。假设 mongod 即将启动的位置存在一个名为 db 的子目录,验证所有内容是否安装得当的过程很简单,如图 1 所示。

image: Firing up Mongod.exe to Verify Successful Installation

图 1 启动 mongod.exe 以验证安装是否成功

如果该目录不存在,MongoDB 并不会创建它。注意在 Windows 7 界面中,当启动 MongoDB 时,会弹出常见的“该应用程序要打开端口”对话框。请确保能访问到该端口(默认情况下指 27017),或者最多是难以连接到该端口。(在后面一篇文章中,我会讨论将 MongoDB 投入生产环境,其中将详细论述到这一问题。)

服务器进入运行状态后,通过 Shell 连接到该服务器的过程非常简单:mongo.exe 应用程序启动一个命令行环境,在该环境中便可直接与服务器交互,如图 2 所示。

image: Mongo.exe Launches a Command-Line Environment that Allows Direct Interaction with the Server

图 2 mongo.exe 启动一个命令行环境,用于直接与服务器交互

默认情况下,Shell 连接到“test”数据库。由于此处目的只是验证是否一切运行正常,因此使用 test 数据库就够了。当然,在这里可以轻松地创建一些简单的示例数据以用于 MongoDB,例如创建一个描述某人的快速对象。在 MongoDB 中查看数据的启动过程非常简单,如图 3 所示。

image: Creating Sample Data

图 3 创建示例数据

本质上,MongoDB 使用 JavaScript Object Notation (JSON) 作为其数据表示法,这种表示法能表现 MongoDB 的灵活性,并可说明 MongoDB 与客户端的交互方式。在内部,MongoDB 以 BSON(JSON 的二进制超集)存储数据,目的是简化存储和索引。JSON 保留了 MongoDB 的首选输入/输出格式,并且通常是在 MongoDB 网站和 wiki 上使用的文档格式。如果不熟悉 JSON,最好是在“深陷”MongoDB 之前充一下电。(开个玩笑。)同时,查看 mongod 用来存储数据的目录,您会发现其中一对以“test”命名的文件。

言归正传,该编写一些代码了。退出 Shell 简单得只需键入“exit”,而关闭服务器也只需在窗口中按 Ctrl+C 或直接关闭窗口:服务器捕获到关闭信号并正确关闭所有内容,然后退出进程。

MongoDB 的服务器(以及 Shell,尽管它微不足道)是用地道的 C++ 应用程序(还记得吗?)编写的,因此访问该服务器需要使用某种 .NET Framework 驱动程序,此类驱动程序知道如何通过打开的套接字进行连接以向服务器输送命令和数据。MongoDB 程序包中并未绑定 .NET Framework 驱动程序,但有幸的是,社区提供了一个,此处的“社区”指的是名叫 Sam Corder 的开发人员,他构建了一个 .NET Framework 驱动程序以及 LINQ 支持来访问 MongoDB。他的作品同时以源代码形式和二进制形式提供,位于 github.com/samus/mongodb-csharp。可以从该页面下载二进制文件(查找页面右上角),也可以下载源代码,然后自行编译。无论采取哪种方式,都会产生两个程序集:MongoDB.Driver.dll 和 MongoDB.Linq.dll。通过向对应项目的“引用”节点快速添加引用后,就可以使用 .NET Framework 了。

编写代码

从根本上来说,打开与正在运行的 MongoDB 服务器的连接,同打开与任何其他数据库的连接没有太大差别,如图 4 所示。

图 4 打开与 MongoDB 服务器的连接

using System;
using MongoDB.Driver; 

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      Mongo db = new Mongo();
      db.Connect(); //Connect to localhost on the default port
      db.Disconnect();
    }
  }
}

查找先前创建的对象并不难,只是与以前 .NET Framework 开发人员使用过的方法有所不同而已(请参阅图 5)。

图 5 查找创建的 mongo 对象

using System;
using MongoDB.Driver; 

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      Mongo db = new Mongo();
      db.Connect(); //Connect to localhost on the default port.
      Database test = db.getDB("test");
      IMongoCollection things = test.GetCollection("things");
      Document queryDoc = new Document();
      queryDoc.Append("lastname", "Neward");
      Document resultDoc = things.FindOne(queryDoc);
      Console.WriteLine(resultDoc);
      db.Disconnect();
    }
  }
}

如果上述内容看起来太突然,别担心,写出这样的代码并非“一日之功”,因为 MongoDB 存储数据的方式与传统数据库是不同的。

对于初学者,请回忆一下,先前插入的数据有三个字段:firstname、lastname 和 age,这三个元素都可作为数据的检索条件。但更重要的是,存储这些数据的行(以强制方式快速完成该过程)为“test.things.save()”,这表示数据被存储在称为“things”的事物中。在 MongoDB 术语中,“things”是一个集合,不言而喻,所有数据都存储在集合中。集合中依次存储着文档,文档则存储着“键/值”对,而其中的“值”又可以是其他集合。在本例中,“things”就是存储在前面提到的 test 数据库内部的集合。

因此,获取数据的过程首先要连接到 MongoDB 服务器,再连接到 test 数据库,然后查找集合“things”。这就是图 5 中前四行的操作:创建一个表示连接的 Mongo 对象,连接到服务器,连接到 test 数据库,然后获取“things”集合。

返回集合之后,代码可通过调用 FindOne 的方式发出一条查询命令来查找单个文档。但与所有数据库一样,该客户端并不想获取集合中的每一个文档,只想查找感兴趣的文档,因此需要对查询进行某种方式的约束。在 MongoDB 中,该约束的实现方式是创建一个 Document,其中包含字段以及要在这些字段中搜索的数据,这是一种称为示例查询(简称 QBE)的概念。由于此处的目标是查找包含 lastname 字段(其值设为“Neward”)的文档,因此需要创建一个仅包含一个 lastname 字段(及其值)的 Document,并作为参数传递给 FindOne。如果查询成功,则返回另一个 Document,其中包含所有相关数据(外加另一个字段);否则返回 null。

顺便提一句,此描述的缩略版可简化为:

Document anotherResult = 
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward"));
       Console.WriteLine(anotherResult);

运行时,不仅会显示传入的原始值,还会显示一个新值,即一个包含 ObjectId 对象的 _id 字段。这是对象的唯一标识符,是在存储新数据时由数据库自动插入的。在尝试修改此对象时,必须避免修改该字段,否则数据库会将该对象视为传入的新对象。通常,这是通过修改由查询返回的 Document 来完成的:

anotherResult["age"] = 39;
       things.Update(resultDoc);
       Console.WriteLine(
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward")));

但是,您始终可以创建新的 Document 实例并手动填入 _id 字段来匹配 ObjectId(如果这样做更合理):

Document ted = new Document();
       ted["_id"] = new MongoDB.Driver.Oid("4b61494aff75000000002e77");
       ted["firstname"] = "Ted";
       ted["lastname"] = "Neward";
       ted["age"] = 40;
       things.Update(ted);
       Console.WriteLine(
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward")));

当然,如果 _id 已知,那么也可将其用作查询条件。

请注意,由于 Document 被有效地非类型化(无类型),因此几乎所有内容均能以任意名称存储在字段中,包括某些核心的 .NET Framework 值类型,如 DateTime。如前所述,从技术角度上讲,MongoDB 用于存储 BSON 数据,其中包括传统 JSON 类型(字符串、整数、布尔值、双精度和 null,不过 null 仅允许用于对象,不允许用于集合)的某些扩展,例如上文提到的 ObjectId、二进制数据、正则表达式以及嵌入式 JavaScript 代码。我们暂时先不管后面两种类型,BSON 能存储二进制数据的这种说法是指能存储任何可简化为字节数组的内容,这实际上表示 MongoDB 能存储任何内容,但可能无法在该二进制 BLOB 中进行查询。

未完待续!

关于 MongoDB,还有太多内容需要讨论,其中包括 LINQ 支持,如何执行更复杂的服务器端查询(超出目前谈论到的 QBE 类型的简单查询功能),以及如何让 MongoDB 在生产服务器场稳定运行。但就现在而言,通过阅读本文并仔细研究 IntelliSense 之后,应该足以让孜孜不倦的程序员入门了。

顺便提一下,如果您有某个特定主题想要了解,欢迎给我留言。毕竟在真正意义上,这是你们的专栏。祝您工作愉快!

Ted Neward 是 Neward & Associates 的负责人,这是一家专门研究 .NET Framework 企业系统和 Java 平台系统的独立公司。他曾写作 100 多篇文章,是 C# 领域最优秀的专家之一并且是 INETA 发言人,著作或合著过十几本书,包括即将出版的《Professional F# 2.0》(Wrox)。他定期担任顾问和导师,请通过 ted@tedneward.com 与他联系,或通过 blogs.tedneward.com 访问其博客。

衷心感谢以下技术专家,感谢他们审阅了本文:Kyle Banker 和 Sam Corder