通过 MongoDB 使用 NoSQL(转)

通过 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 所示。

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

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

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

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

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

图 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 服务器的连接

  1. using System;
  2. using MongoDB.Driver; 
  3.  
  4. namespace ConsoleApplication1
  5. {
  6.   class Program
  7.   {
  8.     static void Main(string[] args)
  9.     {
  10.       Mongo db = new Mongo();
  11.       db.Connect(); //Connect to localhost on the default port
  12.       db.Disconnect();
  13.     }
  14.   }
  15. }

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

图 5 查找创建的 mongo 对象

  1. using System;
  2. using MongoDB.Driver; 
  3.  
  4. namespace ConsoleApplication1
  5. {
  6.   class Program
  7.   {
  8.     static void Main(string[] args)
  9.     {
  10.       Mongo db = new Mongo();
  11.       db.Connect(); //Connect to localhost on the default port.
  12.       Database test = db.getDB("test");
  13.       IMongoCollection things = test.GetCollection("things");
  14.       Document queryDoc = new Document();
  15.       queryDoc.Append("lastname""Neward");
  16.       Document resultDoc = things.FindOne(queryDoc);
  17.       Console.WriteLine(resultDoc);
  18.       db.Disconnect();
  19.     }
  20.   }
  21. }

如果上述内容看起来太突然,别担心,写出这样的代码并非“一日之功”,因为 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。

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

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

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

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

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

  1. Document ted = new Document();
  2.        ted["_id"] = new MongoDB.Driver.Oid("4b61494aff75000000002e77");
  3.        ted["firstname"] = "Ted";
  4.        ted["lastname"] = "Neward";
  5.        ted["age"] = 40;
  6.        things.Update(ted);
  7.        Console.WriteLine(
  8.          db["test"]["things"].FindOne(
  9.            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

 

通过 MongoDB 推动 NoSQL(第 2 部分)

http://msdn.microsoft.com/zh-cn/site/ff714592

Ted Neward

下载示例代码

上一篇文章中, 主要介绍了 MongoDB 的基本知识:安装、运行,以及插入和查找数据。不过,这篇文章只介绍了基本知识,所用的数据对象是简单的名称/值对。这是有道理的,因为 MongoDB 的最大优势就包括可使用相对简单的非结构化数据结构。可以肯定地说,这种数据库能存储的不只是简单的名称/值对。

在本文中,我们将通过一种略微不同的方法来研究 MongoDB(或任何技术)。这个称为探索测试的过程可帮助我们发现服务器中可能存在的错误,同时可以凸显面向对象开发人员在使用 MongoDB 时会遇到的常见问题之一。

前文回顾…

首先,我们要确保讨论同样的问题,还要涉及一些略微不同的新领域。让我们以一种与前一文章 (msdn.microsoft.com/magazine/ee310029) 相比更加结构化的方式来探讨 MongoDB。我们不只是创建简单的应用程序,然后进行调试,我们将采取一举两得的做法,创建探索测试。探索测试的代码段看起来像单元测试,但它们探索功能而不是尝试验证功能。

在 研究一项新技术时,编写探索测试可实现几种不同的目的。其一,它们有助于发现所研究的技术在本质上是不是可以测试的(假设如下:如果难于进行探索测试,则 难于进行单元测试,而这是一个很严重的问题)。其二,在所研究的技术出现新的版本时,它们可作为一种回归测试,因为它们可在旧功能不再正常工作的情况下发 出警告。其三,测试应是相对小型精细的,因此,在本质上,探索测试通过基于以前用例创建新“what-if”用例,使得新技术的学习更为容易。

不 过,与单元测试不同,探索测试不是随应用程序连续开发的,因此,一旦考虑所学习的技术,请将这些测试放在一旁。但不要将它们丢弃,它们还可帮助分离应用程 序代码中的错误与库或框架中的错误。这些测试通过提供一种与应用程序无关的轻型环境来进行实验,从而完成这种分离,不会产生应用程序开销。

明 确了这一点后,我们来创建 Visual C# 测试项目 MongoDB-Explore。将 MongoDB.Driver.dll 添加到程序集引用列表中,然后进行生成,以确保一切正常。(生成时应选择作为项目模板的一部分而生成的 TestMethod。默认情况下,该测试将会通过,因此一切正常,这意味着,如果项目无法生成,则环境中存在问题。检查假设是否正确总是很好的方法。)

看 起来可以立即着手编写代码了,不过,马上会出现一个问题:MongoDB 需要运行外部服务器进程 (mongod.exe),这样客户端代码才能对该进程进行连接,执行有用的操作。我们很容易说“好,好,让我们启动它,然后开始编写代码”,这样还是存 在一个必然的问题。几乎可以肯定,15 个星期后的某个时候,回头再看这些代码,某些糟糕的开发人员(您、我或团队同事)会尝试运行这些测试,看着它们全部失败,然后浪费两三天努力寻找原因,这 才想起看一看服务器是否已运行。

经验教训:尝试以某种方式在测试中捕获所有依赖关系。不管怎样,问题会再次出现在单元测试过程中。因此,我们需要从全新状态的服务器开始,进行一些更改,然后撤消全部更改。要完成这项工作,最简单的方法是停止并启动服务器,现在将问题解决,就为以后节约了时间。

在 测试之前(和/或之后)进行运行操作不是什么新方法,Microsoft 测试和实验室管理器项目可以使用按测试和按测试套件的初始值设定项和清理方法。这些方法包含适用于按测试套件记帐的自定义属性 ClassInitialize 和 ClassCleanup 和适用于按测试记帐的 TestInitialize 和 TestCleanup。(有关详细信息,请参见“使用单元测试”。)因此,按测试套件的初始值设定项将启动 mongod.exe 进程,而按测试套件的清理方法会关闭该进程,如图 1 所示。

图 1 测试初始值设定项和清理方法的部分代码

  1. namespace MongoDB_Explore
  2. {
  3.   [TestClass]
  4.   public class UnitTest1
  5.   {
  6.     private static Process serverProcess;
  7.  
  8.    [ClassInitialize]
  9.    public static void MyClassInitialize(TestContext testContext)
  10.    {
  11.      DirectoryInfo projectRoot = 
  12.        new DirectoryInfo(testContext.TestDir).Parent.Parent;
  13.      var mongodbbindir = 
  14.        projectRoot.Parent.GetDirectories("mongodb-bin")[0];
  15.      var mongod = 
  16.        mongodbbindir.GetFiles("mongod.exe")[0];
  17.  
  18.      var psi = new ProcessStartInfo
  19.      {
  20.        FileName = mongod.FullName,
  21.        Arguments = "--config mongo.config",
  22.        WorkingDirectory = mongodbbindir.FullName
  23.      };
  24.  
  25.      serverProcess = Process.Start(psi);
  26.    }
  27.    [ClassCleanup]
  28.    public static void MyClassCleanup()
  29.    {
  30.      serverProcess.CloseMainWindow();
  31.      serverProcess.WaitForExit(5 * 1000);
  32.      if (!serverProcess.HasExited)
  33.        serverProcess.Kill();
  34.   }
  35. ...

上 述代码第一次运行时,将弹出一个对话框,通知用户正在启动进程。单击“确定”,该对话框就会消失 ... 直到下一次运行该测试。如果不希望显示该对话框,请找到并选中单选框“不再显示此对话框”,以便不再显示该消息。如果正在运行防火墙软件(如 Windows 防火墙),也可能出现该对话框,这是因为服务器需要打开一个端口来接收客户端连接。采用同样的方法处理,所有操作都应以无提示方式运行。如果需要,可在清 理代码的第一行放置一个断点,验证服务器是否正在运行。

只要服务器正在运行,就可开始测试,除非出现另一个问题:每个测试都需要使用自己 的全新数据库,但数据库中预先存在一些数据也是很有用的,这样,更便于进行某些方面(如查询)的测试。每个测试最好都有自己的预先存在的全新数据。包含 TestInitializer 和 TestCleanup 的方法可以完成这一任务。

对此加以讨论之前,我们来看一看这个快速 TestMethod,它尝试确保找到服务器,进行连接,插入、找到和删除对象,使探索测试的速度提高到前一文章所介绍的那样(请参见图 2)。

图 2 TestMethod 确保找到服务器并进行连接

  1. [TestMethod]
  2. public void ConnectInsertAndRemove()
  3. {
  4.   Mongo db = new Mongo();
  5.   db.Connect();
  6.  
  7.   Document ted = new Document();
  8.   ted["firstname"] = "Ted";
  9.   ted["lastname"] = "Neward";
  10.   ted["age"] = 39;
  11.   ted["birthday"] = new DateTime(197127);
  12.   db["exploretests"]["readwrites"].Insert(ted);
  13.   Assert.IsNotNull(ted["_id"]);
  14.  
  15.   Document result =
  16.     db["exploretests"]["readwrites"].FindOne(
  17.     new Document().Append("lastname""Neward"));
  18.   Assert.AreEqual(ted["firstname"], result["firstname"]);
  19.   Assert.AreEqual(ted["lastname"], result["lastname"]);
  20.   Assert.AreEqual(ted["age"], result["age"]);
  21.   Assert.AreEqual(ted["birthday"], result["birthday"]);
  22.  
  23.   db.Disconnect();
  24. }

如 果运行上述代码,运行到声明时,测试将失败。具体来说,问题出在最后一条关于“birthday”的声明。很显然,若将 DateTime 发送到没有时间的 MongoDB 数据库中,是不会正确往返的。进入的数据类型是关联时间为午夜的日期,返回的是关联时间为早上 8 点的日期,这不符合测试末尾处的 AreEqual 声明。

这一点凸显出探索测试的用处,要是不使用探索测试(举例来说,前一文章中的代 码就是这样),可能要到项目进行几个星期或几个月后才会注意到 MongoDB 的这一小特性。这是不是 MongoDB 服务器中的错误是一种价值判断,不需要马上探讨。重要的是,探索测试对技术进行放大观察,有助于隔离这种“有趣的”行为。因此,希望使用该技术的开发人员 可以确定这是不是一个重要更改。有备无患。

顺便提一下,若要修复这段代码从而通过测试,需要将从数据库返回的 DateTime 转换为本地时间。我曾在一个在线论坛中提出这个问题,MongoDB.Driver 的作者 Sam Corder 的回答是:“所有进入的日期都会转换为 UTC,并返回 UTC 时间。”因此,必须将 DateTime 转换为 UTC 时间才能通过 DateTime.ToUniversalTime 进行存储,或者通过 DateTime.ToLocalTime 将从数据库检索的所有 DateTime 转换为本地时区,示例代码如下:

  1. Assert.AreEqual(ted["birthday"], 
  2.   ((DateTime)result["birthday"]).ToLocalTime());

这件事本身凸显了社区的一个极大的优点,即通信双方的距离就是一封电子邮件。

增加复杂性

希望使用 MongoDB 的开发人员需要知道,与最初给人的印象相反,它不是一个对象数据库,也就是说,如果得不到帮助,它无法任意处理复杂对象图。一些常规做法可以提供这种帮助,不过迄今为止,还是需要开发人员才能实现。

例如,考虑图 3 所示的简单对象集合,该集合用于反映很多文档的存储情况,而这些文档描述的是一个有名的家庭。至此不会有什么问题。实际上,执行测试时,测试应向数据库查询插入的对象(如图 4 所示),这是为了确保这些对象是可以检索的。这样,测试通过。真是太妙了。

图 3 简单对象集合

  1. [TestMethod]
  2. public void StoreAndCountFamily()
  3. {
  4.   Mongo db = new Mongo();
  5.   db.Connect();
  6.  
  7.   var peter = new Document();
  8.   peter["firstname"] = "Peter";
  9.   peter["lastname"] = "Griffin";
  10.  
  11.   var lois = new Document();
  12.   lois["firstname"] = "Lois";
  13.   lois["lastname"] = "Griffin";
  14.  
  15.   var cast = new[] {peter, lois};
  16.   db["exploretests"]["familyguy"].Insert(cast);
  17.   Assert.IsNotNull(peter["_id"]);
  18.   Assert.IsNotNull(lois["_id"]);
  19.  
  20.   db.Disconnect();
  21. }

图 4 向数据库查询对象

  1. [TestMethod]
  2. public void StoreAndCountFamily()
  3. {
  4.   Mongo db = new Mongo();
  5.   db.Connect();
  6.  
  7.   var peter = new Document();
  8.   peter["firstname"] = "Peter";
  9.   peter["lastname"] = "Griffin";
  10.  
  11.   var lois = new Document();
  12.   lois["firstname"] = "Lois";
  13.   lois["lastname"] = "Griffin";
  14.  
  15.   var cast = new[] {peter, lois};
  16.   db["exploretests"]["familyguy"].Insert(cast);
  17.   Assert.IsNotNull(peter["_id"]);
  18.   Assert.IsNotNull(lois["_id"]);
  19.  
  20.   ICursor griffins =
  21.     db["exploretests"]["familyguy"].Find(
  22.       new Document().Append("lastname""Griffin"));
  23.   int count = 0;
  24.   foreach (var d in griffins.Documents) count++;
  25.   Assert.AreEqual(2, count);
  26.  
  27.   db.Disconnect();
  28. }

实 际上,这种情况可能不完全是真实的。细致的读者如果键入代码就可能发现,说到底,测试并没有通过,因为预期的对象数与 2 不匹配。这是因为,正如通常的数据库一样,这个数据库的状态在多次调用中保持不变,此外,由于测试代码不显式删除对象,这些对象在各个测试中都存在。

这凸显了面向文档数据库的另外一个特点:完全可能存在重复项,也允许存在重复项。正因为这,每个文档一经插入,都会由 implicit_id 属性进行标记,并且有一个唯一的标识符存储在该属性中,这个唯一标识符实际上会成为文档的主键。

因 此,如果要通过测试,需要在运行每个测试之前清除数据库。尽管删除 MongoDB 存储文件的目录中的文件十分容易,但最好使测试套件能够自动执行这一任务。每个测试都可在完成后以手动方式完成这一任务,时间一长,这会变得有些乏味。测 试代码可利用 Microsoft 测试和实验室管理器的 TestInitialize 和 TestCleanup 功能来捕获常用代码(何不包括数据库连接和断开逻辑),如图 5 所示。

图 5 利用 TestInitialize 和 TestCleanup

  1. private Mongo db;
  2.  
  3. [TestInitialize]
  4. public void DatabaseConnect()
  5. {
  6.   db = new Mongo();
  7.   db.Connect();
  8. }
  9.         
  10. [TestCleanup]
  11. public void CleanDatabase()
  12. {
  13.   db["exploretests"].MetaData.DropDatabase();
  14.  
  15.   db.Disconnect();
  16.   db = null;
  17. }

CleanDatabase 方法的最后一行不是必不可少的,因为下一个测试会用新的 Mongo 对象覆盖该字段引用,不过,有时最好明确表示出该引用不再有内容。用者自慎。重要的是删除在测试中使用过的数据库,清空 MongoDB 用于存储数据的文件,一切都以全新的状态迎接下一个测试。

不过,就目前情况看,该家庭模型是不完整的。所引用的两个人是一对伴侣,假设他们应将对方引用为配偶,如下所示:

  1. peter["spouse"] = lois;
  2.   lois["spouse"] = peter;

如果在测试中运行这段代码,会产生 StackOverflowException。MongoDB 驱动程序序列化程序本身不理解循环引用的概念,它会无休止地引用下去。天哪。这可不是什么好事。

若要修复这一问题,可以在两种方法中选择其一。一种方法是,配偶字段可使用其他文档的 _id 字段来填充(该文档插入后)和更新,如图 6 所示。

图 6 解决循环引用问题

  1. [TestMethod]
  2. public void StoreAndCountFamily()
  3. {
  4.   var peter = new Document();
  5.   peter["firstname"] = "Peter";
  6.   peter["lastname"] = "Griffin";
  7.  
  8.   var lois = new Document();
  9.   lois["firstname"] = "Lois";
  10.   lois["lastname"] = "Griffin";
  11.  
  12.   var cast = new[] {peter, lois};
  13.   var fg = db["exploretests"]["familyguy"];
  14.   fg.Insert(cast);
  15.   Assert.IsNotNull(peter["_id"]);
  16.   Assert.IsNotNull(lois["_id"]);
  17.  
  18.   peter["spouse"] = lois["_id"];
  19.   fg.Update(peter);
  20.   lois["spouse"] = peter["_id"];
  21.   fg.Update(lois);
  22.  
  23.   Assert.AreEqual(peter["spouse"], lois["_id"]);
  24.   TestContext.WriteLine("peter: {0}", peter.ToString());
  25.   TestContext.WriteLine("lois: {0}", lois.ToString());
  26.   Assert.AreEqual(
  27.     fg.FindOne(new Document().Append("_id",
  28.     peter["spouse"])).ToString(),
  29.     lois.ToString());
  30.  
  31.   ICursor griffins =
  32.     fg.Find(new Document().Append("lastname""Griffin"));
  33.   int count = 0;
  34.   foreach (var d in griffins.Documents) count++;
  35.   Assert.AreEqual(2, count);
  36. }

不过,这种方法有一个缺点:它要求将文档插入数据库,并根据需要将它们的 _id 值(在 MongoDB.Driver 中是 Oid 实例)复制到每个对象的配偶字段中。这时,每个文档会再次更新。尽管访问 MongoDB 数据库与传统 RDBMS 更新相比速度是很快的,这种方法仍有些费时。

第二种方法是为每个文档预先生成 Oid 值,填充配偶字段,然后将整个批次发送到数据库,如图 7 所示。

图 7 一种更好的解决循环引用问题的方法

  1. [TestMethod]
  2. public void StoreAndCountFamilyWithOid()
  3. {
  4.   var peter = new Document();
  5.   peter["firstname"] = "Peter";
  6.   peter["lastname"] = "Griffin";
  7.   peter["_id"] = Oid.NewOid();
  8.  
  9.   var lois = new Document();
  10.   lois["firstname"] = "Lois";
  11.   lois["lastname"] = "Griffin";
  12.   lois["_id"] = Oid.NewOid();
  13.  
  14.   peter["spouse"] = lois["_id"];
  15.   lois["spouse"] = peter["_id"];
  16.  
  17.   var cast = new[] { peter, lois };
  18.   var fg = db["exploretests"]["familyguy"];
  19.   fg.Insert(cast);
  20.  
  21.   Assert.AreEqual(peter["spouse"], lois["_id"]);
  22.   Assert.AreEqual(
  23.     fg.FindOne(new Document().Append("_id",
  24.     peter["spouse"])).ToString(),
  25.     lois.ToString());
  26.  
  27.   Assert.AreEqual(2
  28.     fg.Count(new Document().Append("lastname""Griffin")));
  29. }

这种方法仅需要 Insert 方法,因为 Oid 值是提前已知的。顺便提请注意,对声明测试的 ToString 调用是特意进行的,这样,文档会在进行比较之前转换为字符串。

图 7 的代码中,真正务必要注意的是,对通过 Oid 引用的文档解除引用可能比较困难和乏味,因为面向文档这种形式假设文档或多或少是独立实体或分层实体,而不是对象图。(请注意,.NET 驱动程序提供了 DBRef,后者可通过略微更丰富的方式来引用/解除引用其他文档,但仍无法实现对象图友好的系统。)因此,尽管肯定可以获得一个丰富的对象模型并将其存 储到 MongoDB 数据库中,仍不建议这样做。请坚持使用 Word 或 Excel 这样的文档来存储紧密群集的数据组。如果某些内容可视为大型文档或电子表格,则可能非常适合 MongoDB 或其他某种面向文档的数据库。

了解更多内容

我 们已经研究了 MongoDB,在进行总结之前,还需要探索其他一些问题,包括执行谓词查询、聚合、LINQ 支持和一些生产管理说明。我们将在下月探讨这些问题。(敬请期待这篇文章的丰富内容!)同时,我们还会探索 MongoDB 系统,如果对以后的专栏文章有任何建议,欢迎向我发送电子邮件。      

 

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

衷心感谢以下技术专家对本文的审阅: Sam Corder

 

通过 MongoDB 推动 NoSQL(第 3 部分)

http://msdn.microsoft.com/zh-CN/site/165c0f6c-1640-442e-82bd-11cffb606d2e

Ted Neward

下载示例代码

上 一次,我使用探索测试继续对 MongoDB 进行探讨。我介绍了如何在测试期间启动和停止服务器,然后介绍了如何获取跨文档引用,并探讨了导致如此麻烦举动的原因。现在我们需要探索更多中间的 MongoDB 功能:谓词查询、聚合函数以及 MongoDB.Linq 程序集提供的 LINQ 支持。我还将提供一些有关在生产环境中安置 MongoDB 的注意事项。

当我们最终离开我们的主角 .. .

为了节省空间,我就不回顾前文的内容了。您可以从 msdn.microsoft.com/magazine五月六月期刊中找到这两篇文章。但在相关的代码包中,我充实了探索测试的内容,使用我最喜欢的电视节目里的人物,加入了一组已存在的示例数据集以供处理。图 1 显示了之前的探索测试,已经过刷新器处理。到目前为止一切顺利。

图 1 示例探索测试

  1. [TestMethod]
  2.         public void StoreAndCountFamilyWithOid()
  3.         {
  4.           var oidGen = new OidGenerator();
  5.           var peter = new Document();
  6.           peter["firstname"] = "Peter";
  7.           peter["lastname"] = "Griffin";
  8.           peter["_id"] = oidGen.Generate();
  9.  
  10.           var lois = new Document();
  11.           lois["firstname"] = "Lois";
  12.           lois["lastname"] = "Griffin";
  13.           lois["_id"] = oidGen.Generate();
  14.  
  15.           peter["spouse"] = lois["_id"];
  16.           lois["spouse"] = peter["_id"];
  17.  
  18.           var cast = new[] { peter, lois };
  19.           var fg = db["exploretests"]["familyguy"];
  20.           fg.Insert(cast);
  21.  
  22.           Assert.AreEqual(peter["spouse"], lois["_id"]);
  23.           Assert.AreEqual(
  24.             fg.FindOne(new Document().Append("_id",
  25.               peter["spouse"])).ToString(), lois.ToString());
  26.  
  27.           Assert.AreEqual(2,
  28.             fg.Count(new Document().Append("lastname""Griffin")));
  29.         }

呼叫所有老人 .. .

在 前面的文章中,客户端代码已经获得所有匹配特定标准的文档(例如“lastname”字段匹配特定的 String 或“_id”字段匹配特定的 Oid),但我还没有介绍如何进行谓词查询(例如“找到所有‘age’字段的值大于 18 的文档”)。事实上,MongoDB 并不使用 SQL 风格的接口来描述要执行的查询,而是使用 ECMAScript/JavaScript,而且它能接受要在服务器上执行的代码块以筛选或聚合数据,几乎就像存储过程一样。

这提供了一些类似 LINQ 的功能,甚至不用了解 Mongo.Linq 程序集支持的 LINQ 功能我们就能看出来。通过指定包含名为“$where”的字段的文档和描述要执行的 ECMAScript 代码的代码片段,可以随意创建复杂的查询:

 

  1. [TestMethod]
  2.         public void Where()
  3.         {
  4.           ICursor oldFolks =
  5.             db["exploretests"]["familyguy"].Find(
  6.             new Document().Append("$where"
  7.             new Code("this.gender === 'F'")));
  8.           bool found = false;
  9.           foreach (var d in oldFolks.Documents)
  10.             found = true;
  11.           Assert.IsTrue(found, "Found people");
  12.         }

正如您所看到的,Find 调用返回 ICursor 实例,尽管该实例本身不是 IEnumerable(表示它无法用在 ForEach 循环中),却包含一个类型为 IEnumerable<Document> 的 Documents 属性。如果查询会返回很大的数据集,通过将其 Limit 属性设置为 n,可以要求 ICursor 返回前 n 个结果。

谓词查询的语法共有四种格式,如图 2 所示。

图 2 四种不同的谓词查询语法

  1. [TestMethod]
  2.         public void PredicateQuery()
  3.         {
  4.           ICursor oldFolks =
  5.             db["exploretests"]["familyguy"].Find(
  6.             new Document().Append("age",
  7.             new Document().Append("$gt"18)));
  8.           Assert.AreEqual(6, CountDocuments(oldFolks));
  9.  
  10.           oldFolks =
  11.             db["exploretests"]["familyguy"].Find(
  12.             new Document().Append("$where",
  13.             new Code("this.age > 18")));
  14.           Assert.AreEqual(6, CountDocuments(oldFolks));
  15.  
  16.           oldFolks =
  17.             db["exploretests"]["familyguy"].Find("this.age > 18");
  18.           Assert.AreEqual(6, CountDocuments(oldFolks));
  19.  
  20.           oldFolks =
  21.             db["exploretests"]["familyguy"].Find(
  22.             new Document().Append("$where",
  23.             new Code("function(x) { return this.age > 18; }")));
  24.           Assert.AreEqual(6, CountDocuments(oldFolks));
  25.         }

在第二种和第三种格式中,“this”指的是要查询的对象。

事实上,您可以使用文档传达查询或命令,以便从驱动程序向数据库发送任意命令(即 ECMAScript 代码)。例如,IMongoCollection 接口提供的 Count 方法就是对这段冗长代码的简便替代方式:

  1. [TestMethod]
  2.         public void CountGriffins()
  3.         {
  4.           var resultDoc = db["exploretests"].SendCommand(
  5.             new Document()
  6.               .Append("count""familyguy")
  7.               .Append("query",
  8.                 new Document().Append("lastname""Griffin"))
  9.             );
  10.           Assert.AreEqual(6, (double)resultDoc["n"]);
  11.         }

这意味着 MongoDB 文档介绍的所有聚合操作(例如“distinct”或“group”)都可以通过同一种机制进行访问,虽然 MongoDB.Driver API 未将它们作为方法提供。

您可以通过“特殊名称”语法“$eval”将查询之外的任意命令发送到数据库,这样就可以对服务器执行任何合法的 ECMAScript 代码块,仍旧很像存储过程:

  1. [TestMethod]
  2.         public void UseDatabaseAsCalculator()
  3.         {
  4.           var resultDoc = db["exploretests"].SendCommand(
  5.             new Document()
  6.               .Append("$eval"
  7.                 new CodeWScope { 
  8.                   Value = "function() { return 3 + 3; }"
  9.                   Scope = new Document() }));
  10.           TestContext.WriteLine("eval returned {0}", resultDoc.ToString());
  11.           Assert.AreEqual(6, (double)resultDoc["retval"]);
  12.         }

或 者直接对数据库使用所提供的 Eval 函数。如果这还不够灵活,MongoDB 允许在特殊的数据库集合“system.js”中添加 ECMAScript 函数,从而在数据库实例上存储用户定义的要在查询时执行的 ECMAScript 函数以及服务器端执行块,如 MongoDB 网站所述。

缺少的 LINQ

C# MongoDB 驱动程序也有 LINQ 支持,允许开发人员编写如图 3 所示的 MongoDB 客户端代码。

图 3 LINQ 支持示例

  1. [TestMethod]
  2.         public void LINQQuery()
  3.         {
  4.           var fg = db["exploretests"]["familyguy"];
  5.           var results = 
  6.             from d in fg.Linq() 
  7.             where ((string)d["lastname"]) == "Brown" 
  8.             select d;
  9.           bool found = false;
  10.           foreach (var d in results)
  11.           {
  12.             found = true;
  13.             TestContext.WriteLine("Found {0}", d);
  14.           }
  15.           Assert.IsTrue(found, "No Browns found?");
  16.         }

而且为了保持 MongoDB 数据库的动态特征,此示例不需要生成代码,只需调用 Linq 以返回可以“启用”MongoDB LINQ 提供程序的对象即可。在我撰写本文时,LINQ 支持还相当粗略,但在本文发表时,它将得到极大改进。新功能的文档和相关示例将在项目网站的 wiki 栏目发布。

发布是一项功能

最重要的是,如果要将 MongoDB 用在生产环境中,对于那些要让生产服务器和服务保持运行的工作人员来说,还有几个问题需要解决,这样才能减轻他们的工作负担。

首 先,需要将服务器进程 (mongod.exe) 安装为服务,因为在生产服务器上一般不允许在交互式桌面会话中运行该进程。因此,mongod.exe 支持一个服务安装选项“--install”,通过该选项可将其安装为服务,然后通过服务面板或命令行“net start MongoDB”启动。但是,截止本文撰写时,--install 命令存在一个小问题:它通过查看执行安装所用的命令行来推断可执行文件的路径,因此必须在命令行中指定完整路径。也就是说,如果 MongoDB 安装在 C:\Prg\mongodb 中,您必须在命令提示符处(使用管理员权限)使用命令 C:\Prg\mongodb\bin\mongod.exe --install 将其安装为服务。

但是,所有的命令行参数,例如“--dbpath”,必须也显示在该安装命令中,这意味着如果端 口、数据文件的路径等等设置发生更改,则必须重新安装服务。幸运的是,MongoDB 支持一个配置文件选项(通过“--config”命令行选项指定),因此通常最好的做法是将配置文件的完整路径传递到服务安装命令,然后在文件中进行其他 所有配置:

  1. C:\Prg\mongodb\bin\mongod.exe --config C:\Prg\mongodb\bin\mongo.cfg --install
  2. net start MongoDB

像往常一样,测试服务是否成功运行的最简便方法就是使用 MongoDB 下载随附的 mongo.exe 客户端来连接服务。由于服务器通过套接字与客户端通信,因此您需要在防火墙中设置通道,以便允许与服务器的通信。

没有您需要的数据机器人

当然,对 MongoDB 服务器的不安全访问不可能是什么好事,因此阻止不需要的访问者访问服务器成为一项重要功能。MongoDB 支持身份验证,但是它的安全系统不像 SQL Server 这样的“大块头”数据库那么精密。

一般来说,第一步是使用 mongo.exe 客户端连接数据库并将管理员用户添加到管理数据库中(该数据库中包含用于运行和管理整个 MongoDB 服务器的数据),从而创建数据库管理登录,如下所示:

  1. > use admin
  2. > db.addUser("dba""dbapassword")

完成之后,所有进一步的操作(甚至是该外壳程序中的操作)都需要经过身份验证,通过在该外壳程序中进行显式身份验证完成:

  1. > db.authenticate("dba""dbapassword")

DBA 现在可以更改数据库并使用前面所示的同一个 addUser 调用来添加用户,从而将用户添加到 MongoDB 数据库中:

  1. > use mydatabase
  2. > db.addUser("billg""password")

通过 Mongo.Driver 连接数据库时,身份验证信息将作为用于创建 Mongo 对象的连接字符串的一部分进行传递,相同的身份验证过程将透明地进行:

  1. var mongo = new Mongo("Username=billg;Password=password");

很自然,密码不应该直接硬编码在代码中或公开存储,而应该使用与所有基于数据库的应用程序相同的密码规则。实际上,整个配置(主机、端口、密码等)应该保存在配置文件中并通过 ConfigurationManager 类进行检索。

扩展到另一些代码

管 理员应该定期查看正在运行的实例,以获得正在运行的服务器的相关诊断信息。MongoDB 支持一个 HTTP 接口用于与数据库交互,该接口运行的端口号比用于普通客户端通信的端口号高 1,000。MongoDB 的默认端口是 27017,因此该 HTTP 接口位于端口 28017,如图 4 所示。

图 4 用于与 MongoDB 交互的 HTTP 接口

该 HTTP 接口还允许更加偏向 REST 风格的通信方法,与 MongoDB.Driver 和 MongoDB.Linq 中的本机驱动程序正相反;MongoDB 网站对此有详细介绍,但用于访问集合内容的 HTTP URL 中需要添加数据库名称和集合名称(用斜线分隔),如图 5 所示。

图 5 用于访问集合内容的 HTTP URL

有关使用 WCF 创建 REST 客户端的详细信息,请参见 MSDN 文章“REST in Windows Communication Foundation (WCF)”。

专家的忠告

MongoDB 是一款发展迅速的产品,这些旨在探索 MongoDB 核心功能的文章有很多内容并未涉猎。尽管 MongoDB 不能直接替代 SQL Server,但在传统的关系数据库管理系统作用有限的领域,它确实是一种可行的存储替代方案。与 MongoDB 相同,mongodb-csharp 项目也处在蓬勃发展之中。撰写本文时,很多新的改进功能已经添加到 Beta 版中,包括使用普通对象处理强类型化的集合以及对 LINQ 支持的重大改进。请密切注意这两个项目。

但现在我们也应该和 MongoDB 说再见了。让我们把注意力转向孜孜不倦的程序员可能不熟悉(也应该存在争议)的其他开发人员领域。但在目前,愉快编程,并记住伟大的开发专家曾说过的“开发人员使用源代码获取知识,进行防御,切勿成为黑客”。

 

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

衷心感谢以下技术专家对本文进行了审阅: Sam CorderCraig Wilson

posted @ 2011-12-09 12:27  董雨  阅读(521)  评论(0编辑  收藏  举报