将 MongoDB 与 Koa 集成
我们已经学习了一些用于通过 MongoDB Shell 执行 CRUD 操作的 MongoDB 查询。现在我们只需要 MongoDB 驱动程序来帮助我们连接到 MongoDB 服务器,并执行与我们在 MongoDB Shell 中执行的相同的 CRUD 操作。我们将在我们的服务器端框架 Koa 应用程序中将此驱动程序安装为一个依赖项。
安装 MongoDB 驱动程序
Node.js 应用程序的官方 MongoDB 驱动程序是 mongodb。它是一个高级 API,构建在低级 API mongodb-core(MongoDB 核心驱动程序)之上。前者是为最终用户设计的,而后者是为 MongoDB 库开发人员设计的。mongodb 包含使 MongoDB 连接、CRUD 操作和身份验证变得简单的抽象和助手,而 mongodb-core 仅包含 MongoDB 拓扑连接的基本管理、核心 CRUD 操作和身份验证。
有关这两个软件包的更多信息,请访问以下站点:
-
MongoDB驱动程序:https://www.npmjs.com/package/mongodb -
MongoDB核心驱动程序:https://www.npmjs.com/package/mongodb-core -
MongoDB驱动程序 API:http://mongodb.github.io/node-mongodb-native/3.0/api/
我们可以使用 npm 安装 MongoDB 驱动程序:
$ npm i mongodb
接下来,我们将在下一节中通过一个简单的示例来了解如何使用它。
使用 MongoDB 驱动程序创建一个简单的应用
让我们使用 MongoDB 驱动程序设置一个简单的应用程序,以执行简单的连接检查。在这个测试中,我们将使用我们在上一章中介绍的 Backpack 构建系统来运行我们的测试。那么,让我们按照以下步骤开始:
-
如上一节所示,安装
MongoDB驱动程序,然后安装Backpack和cross-env:$ npm i backpack-core $ npm i cross-env
-
创建
/src/文件夹作为默认入口目录,并在其中创建一个index.js文件,然后导入MongoDB驱动程序和Node.js的Assert模块,如下所示:// src/index.js import { MongoClient } from 'mongodb' import assert from 'assert' const url = 'mongodb://localhost:27017' const dbName = 'nuxt-app'在这一步中,我们还应该提供
MongoDB连接详细信息:MongoDB服务器的默认地址 mongodb://localhost:27017,以及我们要连接的数据库nuxt-app。请注意,
Assert是Node.js的内置模块,它提供了一组用于单元测试代码的断言函数,因此我们不必安装此模块。如果你想了解更多关于此模块的信息,请访问 https://nodejs.org/api/assert.html#assert_assert。 -
接下来,建立与
MongoDB服务器中数据库的连接,并使用Assert确认连接,如下所示:// src/index.js MongoClient.connect(url, { useUnifiedTopology: true, useNewUrlParser: true }, (err, client) => { assert.equal(null, err) console.log('Connected to the MongoDB server') const db = client.db(dbName) client.close() })在这个例子中,我们使用了
assert模块的equal方法来确保在通过客户端(client)回调创建数据库实例之前,err回调为null。每当我们完成一个任务时,都应该使用close方法关闭连接。 -
如果你在终端上使用
npm run dev运行此连接测试,你应该在终端上得到以下输出:Connected to the MongoDB server
你可以在我们的
GitHub仓库的/chapter-9/mongo-driver/目录下找到这个简单的例子。
请注意,我们正在连接到 MongoDB,而没有任何身份验证,因为我们尚未保护我们的 MongoDB。你将在本书的最后一章(第 18 章 “使用 CMS 和 GraphQL 创建 Nuxt 应用程序”)中学习如何设置新的管理用户来保护你的 MongoDB。为了简化你的学习曲线并加快本章后续部分的开发过程,我们将选择不保护 MongoDB。现在,让我们在下一节中更深入地了解如何配置 MongoDB 驱动程序。
配置 MongoDB 驱动程序
从上一节的代码中可以看出,每当我们执行 MongoDB CRUD 任务时,都应该导入 MongoClient,提供 MongoDB 服务器 URL、数据库名称等等。这可能会很繁琐且效率低下。让我们按照以下步骤将前面的 MongoDB 连接代码抽象成一个类:
-
将数据库连接详细信息抽象到一个文件中:
// server/config/mongodb.js const database = { host: 'localhost', port: 27017, dbname: 'nuxt-app' }; export default { host: database.host, port: database.port, dbname: database.dbname, url: 'mongodb://' + database.host + ':' + database.port }; -
创建一个
class函数来构建数据库连接,这样我们在执行CRUD操作时就不必重复这个过程。我们还在类函数中构建了一个objectId属性,用于存储ObjectId方法,我们将需要使用该方法来解析来自客户端的ID数据,以便该ID数据可以从字符串转换为对象:// server/mongo.js import mongodb from 'mongodb'; import config from './config/mongodb'; const MongoClient = mongodb.MongoClient; export default class Mongo { constructor() { this.connection = null; this.objectId = mongodb.ObjectId; } async connect() { this.connection = await MongoClient.connect(config.url, { useUnifiedTopology: true, useNewUrlParser: true, }); return this.connection.db(config.dbname); } close() { this.connection.close(); } } -
导入该类并使用
new语句实例化它,如下所示:import Mongo from './mongo' const mongo = new Mongo()例如,我们可以在需要连接到
MongoDB数据库以执行CRUD操作的API路由中导入它,如下所示:// server/routes.js import Router from 'koa-router' import Mongo from './mongo' const mongo = new Mongo() const router = new Router({ prefix: '/api' }) router.post('/user', async (ctx, next) => { //... })
在使用 MongoDB 驱动程序和我们的服务器端框架 Koa 创建 CRUD 操作之前,我们应该了解 ObjectId 和 ObjectId 方法。让我们深入了解一下。
理解 ObjectId 和 ObjectId 方法
ObjectId 是 MongoDB 用作集合中主键的一种快速生成且很可能唯一的值。它由 12 个字节组成;前 4 个字节是时间戳,用于记录 ObjectId 值的创建时间。它存储在集合中每个文档唯一的 _id 字段中。如果在注入文档时未声明 _id 字段,则会自动生成该字段。另一方面,ObjectId(<hexadecimal>) 是一个 MongoDB 方法,我们可以使用它来返回一个新的 ObjectId 值,以及将字符串解析为 ObjectId 对象。以下是一个示例:
// 伪代码
var id = '5d2ba2bf089a7754e9094af5'
console.log(typeof id) // string
console.log(typeof ObjectId(id)) // object
在上面的伪代码中,你可以看到我们使用 ObjectId 方法创建的对象中的 getTimestamp 方法来获取 ObjectId 值中的时间戳。以下是一个示例:
// 伪代码
var object = ObjectId(id)
var timestamp = object.getTimestamp()
console.log(timestamp) // 2019-07-14T21:46:39.000Z
有关 ObjectId 和 ObjectId 方法的更多信息,请查看以下链接:
现在,让我们在接下来的章节中使用 MongoDB 驱动程序编写一些 CRUD 操作。首先,我们将编写注入文档的操作。
注入一个文档
在我们开始之前,我们应该看一下我们将要创建的每个路由所需的代码结构:
// server/routes.js
router.get('/user', async (ctx, next) => {
let result;
try {
const connection = await mongo.connect();
const collectionUsers = connection.collection('users');
result = await collectionUsers...
mongo.close();
} catch (err) {
ctx.throw(500, err);
}
ctx.type = 'json';
ctx.body = result;
});
让我们讨论一下这个结构:
-
捕获和抛出错误:
当我们使用
async/await语句而不是Promise对象进行异步操作时,我们必须始终将它们包装在try/catch块中以处理错误:try { // async/await 代码 } catch (err) { // 处理错误 } -
连接到
MongoDB数据库和集合:在执行任何
CRUD操作之前,我们必须建立连接并连接到我们要操作的特定集合。在我们的例子中,集合是users:const connection = await mongo.connect() const collectionUsers = connection.collection('users') -
执行 CRUD 操作:
在这里,我们使用
MongoDB API方法来读取、注入、更新和删除用户:result = await collectionUsers... -
关闭 MongoDB 连接:
我们必须确保在 CRUD 操作之后关闭连接:
mongo.close()
现在,让我们使用上述代码结构在以下步骤中注入新用户:
-
创建一个使用
post方法的路由来注入新的用户文档:// server/routes.js router.post('/user', async (ctx, next) => { let result //... }) -
在
post路由内部,在对MongoDB执行CRUD操作之前,对我们从客户端接收到的键和值执行检查:let body = ctx.request.body || {}; if (body.name === undefined) { ctx.throw(400, 'name is undefined'); } if (body.slug === undefined) { ctx.throw(400, 'slug is undefined'); } if (body.name === '') { ctx.throw(400, 'name is required'); } if (body.slug === '') { ctx.throw(400, 'slug is required'); } -
在允许将新文档注入
user集合之前,我们要确保slug值尚不存在。为此,我们需要使用findOneAPI 方法和slug键。如果结果为真,则表示slug值已被其他用户文档占用,因此我们向客户端抛出一个错误:const found = await collectionUsers.findOne({ slug: body.slug }); if (found) { ctx.throw(404, 'slug has been taken'); } -
如果
slug是唯一的,那么我们使用insertOneAPI 方法注入一个包含所提供数据的新文档:
result = await collectionUsers.insertOne({
name: body.name,
slug: body.slug
})
在注入文档之后,接下来我们需要获取和查看我们注入的文档,我们将在下一节中进行此操作。
获取所有文档
在将用户添加到 users 集合后,我们可以通过我们在第八章 “添加服务器端框架” 中创建的路由检索所有用户或仅检索其中一个用户。现在我们只需要使用与上一节相同的代码结构来重构这些路由,以便从数据库中获取真实数据:
-
使用
get方法重构列出所有用户文档的路由:// server/routes.js router.get('/users', async (ctx, next) => { let result //... }) -
在
get路由内部,使用findAPI 方法从user集合中获取所有文档:result = await collectionUser.find({ }, { // 排除一些字段 }).toArray()如果你想从查询结果中排除某些字段,可以使用
projection键,并将你不希望在结果中显示的字段的值设置为0。例如,如果你不希望结果中每个文档都显示_id字段,请执行以下操作:projection:{ _id: 0 } -
使用
get方法重构获取单个用户文档的路由:// server/routes.js router.get('/users/:id', async (ctx, next) => { let result //... }) -
使用
findOne方法和_id获取单个文档。我们必须使用ObjectId方法解析 id 字符串,我们在类函数的构造(constructor)函数中有一个objectId的副本:const id = ctx.params.id result = await collectionUsers.findOne({ _id: mongo.objectId(id) }, { // 排除一些字段 })
mongo.objectId(id) 方法将 id 字符串解析为 ObjectID 对象,然后我们可以使用它从集合中查询文档。现在我们可以获取我们创建的文档了,接下来我们需要做的是更新它们。让我们在下一节中进行此操作。
更新一个文档
在向 users 集合添加用户后,我们也可以使用与上一节相同的代码结构在以下步骤中更新它们:
-
创建一个使用
put方法的路由来更新现有的用户文档,如下所示:// server/routes.js router.put('/user', async (ctx, next) => { let result //... }) -
在更新文档之前,我们要确保
slug值是唯一的。因此,在put路由内部,我们使用带有$ne的findOneAPI 查找匹配项,以排除我们正在更新的文档。如果没有匹配项,那么我们继续使用updateOneAPI 方法更新文档:
const found = await collectionUser.findOne({
slug: body.slug,
_id: { $ne: mongo.objectId(body.id) }
});
if (found) {
ctx.throw(404, 'slug has been taken');
}
result = await collectionUser.updateOne({
_id: mongo.objectId(body.id)
}, {
$set: { name: body.name, slug: body.slug },
$currentDate: { lastModified: true }
});
在这个 CRUD 操作中,我们使用了三个操作符:$set 操作符、$currentDate 操作符和 $ne 选择器。这些是你经常用于更新文档的一些更新操作符和查询选择器:
-
更新操作符:
$set操作符用于将字段的值替换为新的指定值,格式如下:{ $set: { <field1>: <value1>, ... } }$currentDate操作符用于将当前日期设置为指定的字段,可以是BSON日期类型(默认)或BSON时间戳类型,格式如下:{ $currentDate: { <field1>: <typeSpecification1>, ... } }有关这两个以及其他更新操作符的更多信息,请访问 有关这两个以及其他更新操作符的更多信息,请访问 https://docs.mongodb.com/manual/reference/operator/update/。
-
查询选择器:
$ne选择器用于选择字段值不等于指定值的文档,包括那些不包含该字段的文档。以下是一个示例:db.user.find( { age: { $ne: 18 } } )此查询将选择
user集合中age字段值不等于18的所有文档,包括那些不包含age字段的文档。有关此选择器和其他查询选择器的更多信息,请访问 https://docs.mongodb.com/manual/reference/operator/query/。
现在,让我们看看如何在下一节中删除我们创建的文档。
删除一个文档
最后,我们将使用与上一节相同的代码结构,通过以下步骤从 users 集合中删除现有用户:
-
创建一个使用
del方法的路由来删除现有的用户文档:// server/routes.js router.del('/user', async (ctx, next) => { let result //... }) -
在使用
deleteOneAPI 方法删除文档之前,在del路由内部,像往常一样,我们使用findOneAPI 方法查找该文档,以确保它首先存在于user集合中:let body = ctx.request.body || {}; const found = await collectionUser.findOne({ _id: mongo.objectId(body.id) }); if (!found) { ctx.throw(404, 'no user found'); } result = await collectionUser.deleteOne({ _id: mongo.objectId(body.id) });
做得好!你已经成功地完成了 MongoDB CRUD 操作的编写,并将它们集成到了 API (Koa) 中。本章的最后一部分涉及将这些操作与 Nuxt 页面集成。让我们在下一节中进行此操作。