2016 年 2 月

第 31 卷,第 2 期

此文章由机器翻译。

工作的程序员-如何成为 MEAN: 深入了解 MongoDB

通过 Ted Neward | 2016 年 2 月

Ted Neward欢迎回来,"MEANies。"(我决定听起来比"Nodeists,",此外,更好开头这部分内容,我们要跳过只是节点。)

代码已达到重要转折点 — 现在,准备就绪,可验证的功能保持不变,有一些测试,则可以安全地开始某些严重重构。特别是,当前内存中数据存储系统会以快速方便演示,更好,但它不会随着时间的推移能够很好地向上扩展。(更不必说所有方法都是虚拟机重新启动/重新启动一次,并且您丢失了所有数据,不在启动代码中的硬编码。) 平均值是时间研究一下"M": MongoDB。

Hu MongoDB ou

首先,它是一定要认识到,受欢迎的座右铭,根据 MongoDB 实际获取其名称从单词"humongous。" 不生成该位 Internet 琐碎内容是否,则返回 true,它的作用下划线,MongoDB 以提供完全相同的功能集与平均值的关系数据库。Mongo,可伸缩性排名很高,与为此,MongoDB 是愿意牺牲一些一致性"最终一致性"。 为支持群集间来换取 ACID 事务功能

此系统可能不会不断到达小数位数为啦删除的记录。事实上,我将非常震惊如果不断出现的是需要地大喊大叫范围之内。但是,MongoDB 有兴趣"这是值得探讨"刻度上排名同样高且其数据模型的另一项功能: 它是面向文档的数据库。这意味着,而不是传统关系表和列强制架构执行模型,MongoDB 使用"无模式"的文档收集到集合的数据模型。这些文档都包含在 JSON 中,并为每个此类文档组成的值可以是传统的名称 / 值对数据类型 (字符串、 整数、 浮点值、 布尔值和等等),以及更多"复合"数据类型 (数组的任何数据类型只是列出或反过来可将名称 / 值对的子对象)。这意味着的时机,数据建模将稍有不同比您想像与关系数据库中; 如果是您唯一的体验现在,这些差异不会很明显,但是这是一个使用更复杂的存储需求时,需要注意,此应用程序是足够小。

注意: 有关 MongoDB 从.NET 开发人员的角度深入了解一下,请查看本专栏 201 三部分构成的系列 MongoDB (bit.ly/1J7DjOB)。

数据设计

从设计角度看,看到"persons"数据模型将如何针对 MongoDB 映射是简单明了: 将"persons"集合,并且在每个文档将为基于 JSON 的程序包的名称 / 值对,以此类推。

这也这么多了。没开玩笑,这是面向文档的数据库感到满意这种偏向开发社区中的原因的一部分 — 为将数据传递到它们的启动曲线是极其低时,它们对应的基于架构的关系进行比较。这有其自己的缺陷,太,当然 — 一个拼写错误和突然的所有查询本来应该能够基于"firstName"突然即将返回空,因为任何文档不具有字段"firstName",但我将介绍几种方法可以减轻这些更高版本的部分。

现在,让我们看一下执行和跳出执行 MongoDB 获取一些数据。

数据访问

第一步是让应用程序能够与 MongoDB; 进行通信对于涉及,无需诧异,安装新的 npm 程序包名为"mongodb"。 因此,到目前为止,本练习中看起来应几乎自动:

npm install --save mongodb

Npm 工具将改动通过其常见的重复性工作和它返回时,Node.js MongoDB 驱动程序安装到 node_modules 目录。如果从 npm 获取一条警告有关 kerberos 程序包不安装 ("mongodb-core@1.2.28 需要 kerberos@~0.0 的对等"),这是一个已知的 bug,看起来可修复,只需安装 kerberos 直接通过 npm ("npm 安装 kerberos")。就不会有任何问题,除此之外,但当然,这可能会发生任何这些包的下一个版本的所有 — 在研究边缘上开发的乐趣就是这样。

接下来,代码将需要打开与 MongoDB 实例的连接。如果实例所在,但是,值得稍微讨论。

数据位置

如在本系列中第一篇文章中所述,没有两个简单选项 mongodb: 一个是要运行它可在本地,这是非常适合开发体验,但因此不适用于生产的体验;另一个是运行在云中,这非常有用的生产体验而无需开发。(如果虽然我在飞机上为会议路上,我不能运行的代码,然后它不提供大量的开发人员体验,在我看来。) 不是事务不正常状态,并且因为它将为任何应用程序的此处的解决方案是非常相似: 在开发期间以及云环境带来的生产或测试中本地运行。

像大多数数据库中,连接到 MongoDB 将要求服务器 DNS 名称或 IP 地址、 数据库名称和 (可选) 若要使用的端口。通常,在开发中,这将是"localhost,"数据库名称和"27017"(MongoDB 默认),但显然将不同于在云中的 MongoDB 实例的设置。例如,名为"msdn 平均值"我 Mongolab MongoDB 实例的服务器和端口设置分别"ds054308.mongolab.com"和"54308,"。

捕获在世界上是创建独立的 JS 文件 (通常称为 config.js) 的节点,要求到 app.js 代码中,此分歧的最简单方法如下所示:

// Load modules
var express = require('express'),
  bodyParser = require('body-parser'),
  debug = require('debug')('app'),
  _ = require('lodash');
// Go get your configuration settings
var config = require('./config.js');
debug("Mongo is available at",config.mongoServer,":",config.mongoPort);
// Create express instance
var app = express();
app.use(bodyParser.json());
// ... The rest as before

保留其他哪些东西,然后,为配置文件以确定在其中运行此应用程序; 环境若要这样做在 Node.js 环境中的常用方法是检查环境变量,"ENV"都将设置为一个"prod"(这是"dev,"或"test"(如果存在一个第三个、 QA 为中心的环境)。因此,配置代码需要检查 ENV 环境变量并将正确的值放入导出的模块对象:

// config.js: Configuration determination
//
var debug = require('debug')('config');
debug("Configuring environment...");
// Use these as the default
module.exports = {
  mongoServer : "localhost",
  mongoPort : "27017"
};
if (process.env["ENV"] === "prod") {
  module.exports.mongoServer = "ds054308.mongolab.com";
  module.exports.mongoPort = "54308";
}

请注意,使用"进程"对象 — — 这是标准的 Node.js 对象内任何 Node.js 运行的应用程序,总是隐式地存在和"env"属性用于查找"ENV"环境变量。(尖锐的读者会注意 ExpressJS 代码,当决定哪一个端口使用,则可以可能重构能够使用 config.js 设置,以及该代码段,但我要将此当作练习供您选择,则读取器的功能完全相同的目标。)

到目前为止一切顺利。实际上,更好的;这还将隐式创建配置代码远离主代码库中的很好地的分离。

让我们开始添加和删除数据。

MongoDB Node.js +

大多数数据库中,您需要打开的连接到 MongoDB,保持该对象,和一样,用于对数据库的后续操作。因此,这看起来很明显的第一步将创建该对象,如应用程序启动并将其存储在全局范围内,如中所示 图 1

图 1 在 MongoDB 中创建的对象

// Go get your configuration settings
var config = require('./config.js');
debug("Mongo is available at ",config.mongoServer,":",config.mongoPort);
// Connect to MongoDB
var mongo = null;
var persons = null;
var mongoURL = "mongodb://" + config.mongoServer +
  ":" + config.mongoPort + "/msdn-mean";
debug("Attempting connection to mongo @",mongoURL);
MongoClient.connect(mongoURL, function(err, db) {
  if (err) {
    debug("ERROR:", err);
  }
  else {
    debug("Connected correctly to server");
    mongo = db;
    mongo.collections(function(err, collections) {
      if (err) {
        debug("ERROR:", err);
      }
      else {
        for (var c in collections) {
          debug("Found collection",collections[c]);
        }
        persons = mongo.collection("persons");
      }
    });
  }
});
// Create express instance
var app = express();
app.use(bodyParser.json());
// ...

请注意连接呼叫采用 URL 和回调 — 此回调将错误对象和数据库连接对象作为其参数作为是 Node.js 约定。如果第一个是算作 undefined 或是 null,则会发生错误,否则一切都糟透 swimmingly。该 URL 是 MongoDB 特定 URL,使用"mongodb"方案中,但否则看上去非常像传统的 HTTP URL。

但是,还有可能不明显此代码很微妙的地方开始时: 会调用该回调在某一时刻良好的启动代码的其余部分完成后,则该命令将更加明显,当您查看调试打印输出中,如中所示 图 2

调试打印的输出
图 2 调试打印的输出

请参阅"示例应用程序侦听"消息的显示方式的"已连接到服务器的正确"消息之前从该回调? 假设在应用程序启动时出现此问题,此并发问题不是很关键,但它不会消失,,这是,毫无疑问,使用 Node.js 的最棘手的部分之一。的确如此,您的 Node.js 代码将永远不会同时在两个线程在执行的同时,但这并不意味着不会有一些有趣的并发问题,需要在这里;它们只是看起来不同于您已经习惯的.NET 开发人员。

此外,正如快速提醒,此代码针对全新的 MongoDB 数据库集合循环的第一次运行时将为空,MongoDB 不会创建集合 (或甚至数据库!) 直到它绝对出现,这通常会发生有人将写入到其中。完成插入后,MongoDB 将创建必要的项目和数据结构来存储数据。

无论如何,目前,我们有了数据库连接。若要更新的 CRUD 方法,若要开始使用它的时间。

插入

InsertPerson 将 MongoDB 集合对象上使用 insert 方法,并且,同样,你需要使用数据库操作的结果调用的回调:

var insertPerson = function(req, res) {
  var person = req.body;
  debug("Received", person);
  // person.id = personData.length + 1;
  // personData.push(person);
  persons.insert(person, function(err, result) {
    if (err)
      res.status(500).jsonp(err);
    else
      res.status(200).jsonp(person);
  });
};

请注意的注释掉的代码 (从内存中数据库版本我要将迁移离开物理办公室);我可以保留它搁置在那里专门用于证明一个点。MongoDB 将创建标识符字段中,"_id"需要在数据库中,文档的主键,因此我的令人难以置信不良自行开发"id"生成器代码不是仅不再有必要,但完全不需要。

此外,请注意,在函数中的最后一个语句是具有相关联的回调的插入方法。虽然不是必需的这是在函数块的最后一个语句,了解 insertPerson 函数将终止之前完成数据库插入至关重要。这样,您不想给调用方返回任何内容,直到您知道数据库操作成功与否有 Node.js 的基于回调的性质,因此对由于"res"的调用不发生任何地方回调过程之外。(有人怀疑应通过将调试调用放在 persons.insert 调用,并在该回调本身,另一个后说服自己的这并查看第一个激发回调执行之前。)

检索所有

插入要求进行验证,因此虽然在这里,我将重构 getAllPersons,只是需要快速查询到该集合中查找所有文档的集合:

var getAllPersons = function(req, res) {
  persons.find({}).toArray(function(err, results) {
    if (err) {
      debug("getAllPersons--ERROR:",err);
      res.status(500).jsonp(err);
    }
    else {
      debug("getAllPersons:", results);
      res.status(200).jsonp(results);
    }
  });
};

继续之前,还有几个快速需要注意的事项: 首先,查找调用选取描述您想要查询的集合,在这种情况下,我将保留为空,则为所依据的条件的谓词文档如果这是按名字的查询,该谓词文档需要如下所示:

"{ 'firstName':'Ted' }"

其次要注意,查找从返回的对象不是实际结果定位,因此需要调用 toArray 将其转换为内容使用。ToArray 采用回调,并再次重申,回调的每个分支必须确保返回到调用方使用 res.status ().jsonp 信息传达。

中间件

我可以继续之前,请从以前的专栏 getPerson、 updatePerson 和 deletePerson 函数所有召回取决于 personId 中间件函数查找某个人的标识符。这意味着该中间件需要进行更新以查询其 _id 字段 (这是 MongoDB ObjectID,而不是字符串!),而不是查看在内存中数组中,集合中所示 图 3

图 3 更新中间件,若要查询的集合

app.param('personId', function (req, res, next, personId) {
  debug("personId found:",personId);
  if (mongodb.ObjectId.isValid(personId)) {
    persons.find({"_id":new mongodb.ObjectId(personId)})
      .toArray(function(err, docs){
        if (err) {
          debug("ERROR: personId:",err);
          res.status(500).jsonp(err);
        }
        else if (docs.length < 1) {
          res.status(404).jsonp(
            { message: 'ID ' + personId + ' not found'});
        }
        else {
          debug("person:", docs[0]);
          req.person = docs[0];
          next();
        }
      });
  }
  else {
    res.status(404).jsonp({ message: 'ID ' + personId + ' not found'});
  }
});

MongoDB Node.js 驱动程序文档 findOne 方法,这似乎是更合适,但驱动程序文档规定为不推荐使用方法。

请注意中间件,如果它获得了无效的 ObjectId,不会调用下一步。这是代码的一种以保存一些行,因为如果它不是代码的合法的 ID,它可能不能存在,因此,手动重新对查找人员从数据库中有很大的各种方法中 404 的快速方法。这同样适用如果结果具有零个文档 (ID 不是在数据库中的含义)。

检索一个,Delete 和 Update

因此,中间件可 getPerson 微不足道的因为它处理所有可能的错误或未找到文档的条件:

var getPerson = function(req, res) {
  res.status(200).jsonp(req.person);
};

而 deletePerson 是几乎一样简单:

var deletePerson = function(req, res) {
  debug(“Removing”, req.person.firstName, req.person.lastName);
  persons.deleteOne({“_id”:req.person._id}, function(err, result) {
    if (err) {
      debug(“deletePerson: ERROR:”, err);
      res.status(500).jsonp(err);
    }
    else {
      res.person._id = undefined;
      res.status(200).jsonp(req.person);
    }
  });
};

这两种进行 updatePerson 相当可预测:

var updatePerson = function(req, res) {
  debug(“Updating”,req.person,”with”,req.body);
  _.merge(req.person, req.body);
    persons.updateOne({“_id”:req.person._id}, req.person, function(err, result) {
    if (err)
      res.status(500).jsonp(err);
    else {
      res.status(200).jsonp(result);
    }
  });
};

合并调用,顺便说一下,是使用之前,要将属性从请求正文复制到从数据库加载 person 对象的同一个 Lodash 函数。

总结

喔。这一直是有点多于一些其他在系列中,但在这种情况下,有完全现在可以运行针对 MongoDB 数据库,而不是我以前在模拟出期间一直在使用内存中数组的代码。但不是非常、 不绝不算。对于初学者而言,这些查询谓词周围的代码中的任何拼写错误将创建未预料到的运行时错误。更重要的是,作为.NET 开发人员,我们习惯于某种类型的"域对象"以使用,特别是当没有某种形式的验证的对象,必须先完成的各种属性 — — 它不是分散在整个基本代码的速成版部件该验证代码的一个好办法。下一次是 docket 上。但现在 … 祝您编码愉快!


Ted Neward是 iTrellis,基于西雅图的 polytechnology 咨询公司的 CTO。他曾写过 100 多篇文章,是一个 F # MVP、 INETA 发言人,并且具有编写或与他人合著过十几本书。如果您有兴趣请他参与您的团队工作,请通过 ted@tedneward.com 与他联系,或通过 blogs.tedneward.com 访问其博客。

感谢以下技术专家对本文的审阅: Shawn Wildermuth