2016 年 3 月

第 31 卷,第 3 期

此文章由机器翻 译。

工作的程序员如何成为平均: 使用 MongooseJS 的可靠验证

通过 Ted Neward |2016 年 3 月

Ted Neward在我 2016 年 2 月的专栏 (msdn.com/magazine/mt632276),我过渡到 MongoDB 数据库。执行此操作不是那么困难这要归功于 Node.js 和 MongoDB 的基于 JSON 的性质的基于 JSON 的性质。(生命始终是更容易处理的数据,如果转换是从苹果到苹果时)。MongoDB 有一个重要优势在于它将"扩大"和"向外扩展"方便、 更不必说即可轻松地开始。但是,它也有一个显著的缺点 ︰ 由于 MongoDB 是一个"无模式"的数据库 (在于架构已经预定义 — 数据库包含集合,集合会保留文档和文档基本上只是 JSON 对象),在它位于其自己析构的种子。

首先,回想一下,MongoDB 查询也是本质上是查询文档 (find 调用该第一个参数),其中包含进行扫描的匹配项的集合所依据的字段。因此,如果查询"{fristName: Ted}"执行对现有数据库中的"persons"集合,执行任何操作将会返回 — 查询文档中的字段名称是拼写错误 ("fristName"而不是"firstName"),因此它会针对集合中的文档不匹配。(除非有拼写错误文档中在集合中,当然。) 这是无模式的数据库的最大缺点之一 — 简单拼写错误代码或用户输入可以创建非常生气最令人挠头性质的意外错误。这是其中就好了以获得一些语言支持,无论是由编译器或解释器。

其次,请注意,我最后一列中显示的代码,让人想起在计算的"客户端-服务器"纪元的两个几十年来已流行的"两层"应用程序。没有将输入直接从用户 (或,这种情况下,从 API 使用者)、 将其应用和从数据库,它将直接返回给调用方重新获取简单的数据结构的代码层。当然是面向的"对象"在该代码中没有意义。时不处理断字符,那就更好如果我们无法了解更强的"对象状态"为服务器端代码中,以便某些验证的各种属性无法集中在一起,例如。例如: 是合法的人具有空名字或姓氏? 可以他通过执行一切内容在他的状态? 是否有"default"状态,如果用户选择不是为了提供一个,或者是可接受的空状态?

Node.js 出众具有很长一段为这些问题 (它是第一个语言生态系统在大规模基础上采用 MongoDB 之一) 和无需诧异,它提出了一个完善的解决方案的问题,称为 MongooseJS。它是位于"top"的软件层的 MongoDB,并提供了一个架构类似语言验证验证层,不仅能还"域对象"将层构建为服务器端代码的机会。因此,它是某种程度"其他我 '"MEAN 堆栈中。

MongooseJS: 开始

到目前为止,应获取练习非常简单、 直观和重复 ︰ "的操作? 您 npm 这一次" 简短的回答是"npm install-保存 mongoose,"但匹配任何条款按照命令行上的包在 npm 注册表中时将搜索有关确切的包可能会是什么 (人倾向于"thing"和"thingjs"之间 waffle 作为包名称的节点)、"npm 查找"或"npm 搜索"一些混淆。或者,选择的搜索引擎中键入"Mongoose JS"将产生高达 Mongoose Web 站点 (mongoosejs.com),这将会正确 npm 程序的发展,以及大量有关如何使用 Mongoose 文档。(这使得它已加入书签在浏览器中,确定容易做到。)

在安装之后,您可以开始定义 Mongoose"架构"对象,将定义 MongoDB 集合中存储的对象的类型。

Mongoosing 人员

Mongoose 使用什么是实质上是一个两步过程定义在 MongoDB 数据库 API 之上的 JavaScript 对象模型的一些有趣的术语。首先,我们定义了"架构"其外观类似于传统的类从更传统的基于类的语言 (C#、 c + +、 Java 或 Visual Basic)。此架构将具有字段、 定义为这些字段的类型和 (可选) 将值分配给这些字段时包括的字段的一些验证规则。此外可以添加一些方法、 实例或静态,我会谈到更高版本。然后,一旦定义架构对象,则""将其编译到一个模型,它是什么将用于构造这些对象的实例。

记下仔细此处 ︰ 这仍是 JavaScript,,更重要的是,这是 JavaScript 更准确地描述为缺少的"类"无论如何概念的 ECMAScript 5 的版本。因此,即使 Mongoose 方法错觉,您定义"类",它仍是您知道并喜欢 (或 loathe) 的基于原型的语言。这会影响此两步过程的原因 — 首先定义一个对象,将其作为类,第二个要取决于周围"new"运算符的 JavaScript/ECMAScript 5 规则中定义隐式将作为"constructor"或"工厂,"工作对象。

因此,将其转换为代码中,"require ()"ing 平常"mongoose"处的局部变量的 JavaScript 代码的顶部到 Mongoose 库后,您可以使用,以定义新的架构对象 ︰

// Define our Mongoose Schema
var personSchema = mongoose.Schema({
  firstName: String,
  lastName: String,
  status: String
});
var Person = mongoose.model('Person', personSchema);

还有很多事情,您可以执行与 personSchema 中的字段,但对于初学者而言,让我们简单地说;这将不仅帮助到工作代码速度更快,而且也将突出显示几个细微 Mongoose 有关此过程中。此外,同样,请注意模型定义时所依据的双步骤过程 ︰ 首先,定义的架构,使用架构函数/构造函数,然后将架构对象传递到模型函数/构造函数中,以及此模型的名称。Mongoose 用户之间的约定是,从模型调用返回的对象将是相同的因为这就是正在定义的类型将帮助使其成为一个类的效果。

Mongoosing 实操

定义了模型后,现在剩下要做的唯一事情是重写将使用该模型的各种路由方法。启动的最简单位置是 getAllPersons 路由,如中所示 图 1, ,这是因为返回的整个集合从数据库中不包含筛选器。

图 1 重写 getAllPersons 路由

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

请注意,代码从根本上类似于有了以前 — 查询执行和调用 (同样,与约定"err,结果"作为其参数) 传递进来的回调函数时,此查询已完成。但是,现在 Mongoose 提供围绕访问稍微有点更多结构由路由通过 Personmodel 对象,将生成所有类型的功能强大的好处,如您将看到一分钟的时间。

接下来是 personId 中间件,因为使用它在路由,几乎所有的其余部分中所示 图 2

图 2 重写 personld 路由

app.param('personId', function (req, res, next, personId) {
  debug("personId found:",personId);
  if (mongodb.ObjectId.isValid(personId)) {
    Person.findById(personId)
      .then(function(person) {
        debug("Found", person.lastName);
        req.person = person;
        next();
      });
  }
  else {
    res.status(404).jsonp({ message: 'ID ' + personId + ' not found'});
  }
});

同样,这是一个中间件函数,因此的目标是查找从该集合的 Person 对象并将其存储到请求的对象 (必需)。但请注意,在确认传入 personId 有效 MongoDB ObjectId/OID;Person 对象会显示,这次调用 findById,传入 ObjectID/OID 中传递。最受关注的下面是 Mongoose 支持"承诺"语法/样式将装置 JavaScript 的未来版本中,从 findById 返回的对象是一个承诺对象,可以对其调用"then"并传递在回调中完成查询时要执行配置。

这两种方法然后可以执行的操作之前执行此查询的整个主机,如按特定顺序基于字段内的结果,对查询结果进行排序,或选择 (以便隐藏客户端不应收到任何敏感信息) 的字段的一个子集。这些资源是方法调用 wedged 右侧之间查找,然后在流畅样式中,如 ︰

Person.find({ })
  .sort({ 'firstName':'asc', 'lastName':'desc' })
  .select('firstName lastName status')
  .then(function(persons) {
    // Do something with the returned persons
  });

在这种情况下,这会对结果进行排序的升序排序名字、 按姓氏以降序 (并不是执行此操作更有意义) 结果进行排序,然后从中剥离出"firstName",除"lastName"和"status"字段。查询"修饰符"的完整列表是令人印象深刻,包括大量有用的方法,如限制 (在一定数量的结果剪切)、 跳过 (来试图跳过前 n 个结果返回)、 (用于返回聚合返回的文档数) 计数等。

太分心获取程序之前,但是,我将舍入的路由方法的其余。

那些具有使用中间件来获取 Person 对象的 update 和 delete 问题,变得非常简单地利用保存和删除对象本身上提供的 Mongoose 方法。如中所示 图 3, ,只需插入新的用户,需要实例化新的人模型并将保存在其上的方法。

图 3 实例化新的用户模型

var updatePerson = function(req, res) {
  debug("Updating",req.person,"with",req.body);
  _.merge(req.person, req.body);
  // The req.person is already a Person, so just update()
  req.person.save(function (err, person) {
    if (err)
      res.status(500).jsonp(err);
    else {
      res.status(200).jsonp(person);
    }
  });
};
var insertPerson = function(req, res) {
  var person = new Person(req.body);
  debug("Received", person);
  person.save(function(err, person) {
    if (err)
      res.status(500).jsonp(err);
    else
      res.status(200).jsonp(person);
  });
};
var deletePerson = function(req, res) {
  debug("Removing", req.person.firstName, req.person.lastName);
  req.person.delete(function(err, result) {
    if (err) {
      debug("deletePerson: ERROR:", err);
      res.status(500).jsonp(err);
    }
    else {
      res.status(200).jsonp(req.person);
    }
  });
};

从概念上讲,它看起来很类似于上一个、 非 Mongoose 的版本,但没有真正重要的是,如果细微,更改 ︰ 与人员有关的逻辑现在正在多个本地化为人模型 (和 Mongoose 自己的持久性实现)。

Mongoose 验证

现在,开个玩笑,你想要将若干新规则添加到应用程序 ︰ firstName 和 lastName 都不能为空,状态可以仅为几个可能的值之一 (按照枚举类型)。这是其中 Mongoose 的能够创建在服务器端中的域对象模型可以是特别有用,因为经典 O O 思维持有,应在该对象类型本身并不是周围使用代码中封装这些类型的规则。

Mongoose,这是实际没有什么价值。在该架构对象,请转遭到简单名称的字段 ︰ 键入到更复杂的名称对 ︰ 可以在这些对象描述符中指定对象的描述符对和验证规则。除了数值验证 (min 和 max) 和字符串验证 (最小长度和最大长度) 的常用筏拖,可以指定为"status"字段的可接受值的数组,并时没有定义默认值为每个字段指定,其中 (毫不奇怪) 巧妙地适合新的要求,如中所示 图 4

图 4 Mongoose 验证

// Define our Mongoose Schema
var personSchema = mongoose.Schema({
  firstName: {
    type: String,
    required: true,
    default: "(No name specified)"
  },
  lastName: {
    type: String,
    required: true,
    default: "(No name specified)"
  },
  status: {
    type: String,
    required: true,
    enum: [
      "Reading MSDN",
      "WCFing",
      "RESTing",
      "VBing",
      "C#ing"
    ],
    default: "Reading MSDN"
  },
});
var Person = mongoose.model('Person', personSchema);

其他无需任何更改在代码库中的其他任何地方 — 所有这些规则有关用户状态捕获完全他们应在哪里,架构对象的定义中。

如果尝试现在将插入 JSON,程序不会如遵循的状态,右侧的列表 ︰

{
  "firstName":"Ted",
  "lastName":"Neward",
  "status":"Javaing"
}

保存 insertPerson 路由内部调用的方法将产生与返回的 JSON 正文读取中所示的 500 的响应, 图 5

图 5 从上一个保存的验证失败的错误结果

{
  "message":"Person validation failed",
  "name":"ValidationError",
  "errors":{
    "status":{
      "properties":{
        "enumValues":[
          "Reading MSDN",
          "WCFing",
          "RESTing",
          "VBing",
          "C#ing"
        ],
        "type":"enum",
        "message":"`{VALUE}` is not a valid enum value for path `{PATH}`.",
        "path":"status",
        "value":"Javaing"
      },
      "message":"`Javaing` is not a valid enum value for path `status`.",
      "name":"ValidatorError",
      "kind":"enum",
      "path":"status",
      "value":"Javaing"
    }
  }
}

几乎 nails 出了什么问题存在。

Mongoose Methoding

当然,面向对象意味着组合状态和行为,这意味着这些域对象实际上不是对象,除非您可以将方法附加到对象实例或作为一个整体的"类"。这样做是实际上相当简单 ︰ 调用 Mongoose (传统的基于实例的方法的方法),或者某些基于类的静态将方法,定义方法,并传入要为方法体中,使用的函数如下所示 ︰

personSchema.method('speak', function() {
  console.log("Don't bother me, I'm", status);
});

它很不完全类似 C# 中编写的类,但会相当接近。请注意,所有其他更改的架构对象,以及这些之前,必须全部完成架构已编译的模型对象中,因此此调用必须显示在 mongoose.model 调用之前。

Mongoose 版本控制

顺便说一下,如果再次执行到 /persons 的快速 GET,得到的输出将包含一些意外的一些内容 ︰

[{"_id":"5681d8bfddb73cd9ff445ec2",
  "__v":0,
  "status":"RESTing",
  "lastName":"Castro",
  "firstName":"Miguel"}]

"__V"字段中,而 Mongoose 以无提示方式到这一混合环境是怎样被遗漏 (保留到数据库) 是 Mongoose 作为 versionKey 字段中,可以识别的版本控制字段,它可帮助 Mongoose 识别更改到该文档。可以将其视为一个源代码文件,同时做出的更改进行检测的一种方式的版本号。通常情况下,这是只需 Mongoose,内部详细信息,但它可以是有点令人惊讶会显示存在第一次。

不过,将引发更有趣的问题 ︰ 它并不少见系统用于想要跟踪已创建的文档时或当对文档进行了更改并起来肯定会困难需要填写字段,每次周围的代码的任何部分接触或保存这些专家之一。

Mongoose 可以帮助解决此问题,通过允许您"挂钩"某些生命周期方法来更新字段权限,该文档被写入 MongoDB,而不考虑将持久化,如中所示的调用之前 图 6

图 6 挂钩与 Mongoose 的生命周期方法

// Define our Mongoose Schema
var personSchema = mongoose.Schema({
  created: {
    type: Date,
    default: Date.now
  },
  updated: {
    type: Date,
  },
  // ... as before
});
personSchema.pre('save', function(next) {
  // Make sure updated holds the current date/time
  this.updated = new Date();
  next();
});
var Person = mongoose.model('Person', personSchema);

现在,每当构造一个 Person 对象时,它将默认创建的字段来保存当前的日期/时间和已更新的字段将设置为当前日期/时间之前发送到 MongoDB 进行存储。Mongoose 调用这些"中间件,",因为它们在速成版,通过定义的中间件函数的精神,但是不要弄错,它们是特定和包含完全向 Mongoose 定义的对象。

现在,创建或更新某个人时,它必须填写这些相应字段,并根据需要更新。此外,应更复杂 (如一个完备的审核日志) 的内容是必要的是很容易看到无法添加这样的方式。而不是创建/更新字段没有审核日志字段中,这将是一个数组,并且只需为该数组来描述所做的每个时间和/或谁做了,具体取决于要求遍又一遍地附加的"保存"挂钩。

总结

这一直是另一强度相对较高的条,并且肯定没有大量详细的 Mongoose 值得探讨的。但是,这就是与目标同等的探索新的技术堆栈和平台,并且会极其 churlish 的我不要包括这种乐趣到我忠实的读者群。请注意的重要一点是,这种 Mongoose + MongoDB 的组合提供了功能强大的组合 ︰ MongoDB 数据库的无模式特性结合语言强制执行验证的基本类型加上强制运行时执行验证的数据值,以向我们提供一些最佳的这两种静态类型化和动态类型化世界联系起来。这并不是问题和粗糙的位置,但总的来说,很难对我来说很难想象执行 MongoDB 中的而无需阻止愚蠢做事 Mongoose 类似任何严重编码工作。

我刚才几乎都使用服务器端的事物,但我外出时的空间,所以现在为 … 祝您编码愉快 !


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

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