内容


学习 Node.js,第 14 单元:迁移到 Cloudant 数据库即服务

将 Node.js 应用程序数据迁移到 Cloudant — IBM Cloud 高度可扩展的非关系型数据库即服务

Comments

在 Node.js 学习路径的前几个单元中,您使用以下两个本地数据库安装设置了"购物清单"应用程序:SQLite3MongoDB。在当前的技术环境下,了解如何针对云端扩展 Node.js 应用程序也很重要。大多数企业都转而采用云存储,因为服务器托管成本高昂,而且可能会导致可用性问题。通过使用云计算,您只要为所需的云服务买单,因此就不会因无力承受的昂贵硬件而望洋兴叹

Node.js 学习路径的最后一个单元中,了解如何将"购物清单"应用程序的数据迁移到 IBM Cloud 的非关系型数据库即服务(DBaaS)产品 Cloudant。"购物清单"应用程序将继续在您的计算机上本地运行,但您将使用开源 Cloudant 驱动程序(@cloudant/cloudant) 来访问和处理数据。

Cloudant

Cloudant 是一个以文档为导向的分布式非关系型数据库,它是 IBM Cloud 的一部分。它由 2008 年开始的 MIT 项目 衍生而来,并且基于 Apache CouchDB 2.0。虽然 Cloudant 可作为独立安装使用,但它已经发展成为数据库即服务产品。

与 SQLite3(如第 6 单元中所介绍)或 MongoDB(如第 12 单元中所介绍)不同,Cloudant 不使用表或集合来存储数据,而是将所有文档都存储在数据库中。

作为开发者,您可以确定如何对数据进行分区以优化文档搜索和检索。例如,在本单元中,我们将使用 type 属性来区分以下两种不同类型的文档:itemshoppingList

务必查看 Cloudant 文档,了解有关 Cloudant 的更多信息。

结合使用 Cloudant 和 Node.js

在处理示例应用程序时,您需要参考以下工具的文档:

前提条件

您需要一个 IBM Cloud 帐户才能跟随本单元进行操作。如果您已拥有 IBM Cloud 帐户,即表明已完成所有设置。如果您还没有此帐户,可以注册免费帐户。(单击 Create your Lite account 按钮。)

您还需要本单元的源代码,可从 GitHub 获取此源代码。

设置 Cloudant

在本节中,您会将 Cloudant 设置为"购物清单"应用程序的数据库服务。只需执行以下步骤。

1. 登录到 IBM Cloud

转至 IBM Cloud 页面进行登录。

如果您还没有 IBM Cloud 帐户,可转至 IBM Cloud Lite 注册页面获取免费的 Lite 帐户。这个过程很快,因此您可以立即登录并准备好进行编码。

虽然 Lite 帐户受限制,但是您在登录后可以浏览 IBM Cloud 中可用的功能,并运行本单元的所有代码。

2. 创建 Cloudant 数据库服务

在登录到 IBM Cloud 帐户后,您将进入 IBM Cloud 仪表板。

从左上角的汉堡菜单中选择 Infrastructure。在下一个页面上,单击 storage 图标,然后在之后的页面上,单击类别列表中的 Databases。从 Databases 页面上的数据库列表中选择 Cloudant。

接下来,您将创建 Cloudant 服务。为您的服务命名(例如 Node-LP-Cloudant),选择 Use both legacy credentials and IAM 作为认证方法,然后单击 Create 按钮,如图 1 所示。

图 1. 创建 Cloudant 服务
图 1. 创建 Cloudant 服务

图 1. 创建 Cloudant 服务

在创建 Cloudant 服务后,您将进入仪表板,在那里您将看到该服务:

图 2. 显示新创建的 Cloudant 服务的 IBM Cloud 仪表板
图 2. 显示新创建的 Cloudant 服务的 IBM Cloud 仪表板

图 2. 显示新创建的 Cloudant 服务的 IBM Cloud 仪表板

3. 创建凭证

您需要使用凭证才能远程连接到该 Cloudant 服务。要创建凭证,可选择您的 Cloudant 服务,然后从下一个屏幕的左侧菜单中选择 Service credentials。选择 New credential (+) 按钮,如图 3 所示。

图 3. Cloudant 仪表板中的 Service credentials 屏幕
图 3. Cloudant 仪表板中的 Service credentials 屏幕

图 3. Cloudant 仪表板中的 Service credentials 屏幕

在 Add new credential 对话框中,为新凭证指定名称,选择 Auto Generate 作为服务 ID,然后单击 Add

图 4. Add new credential 对话框
图 4. Add new credential 对话框

图 4. Add new credential 对话框

在创建凭证后,该凭证将显示在服务凭证列表中。选择 View credentials,然后将凭证复制到剪贴板,如图 5 所示。

图 5. 复制的凭证
图 5. 复制的凭证

图 5. 复制的凭证

您需要使用复制的凭证来创建本地配置文件,这允许 Node.js 应用程序与 Cloudant 服务进行交互。

4. 创建服务凭证配置文件 (vcap-local.json)

在编辑器(我使用的是 VSCode)中,创建一个新文件,并在其中粘贴您刚从服务凭证屏幕复制的凭证。您会注意到,它是一个 JSON 对象(包括周围的花括号)。

接下来,复制以下代码块并将其粘贴到编辑器中。

清单 1. vcap-local.json 模板

{
    "services": {
        "cloudantNoSQLDB": {
            "credentials":
            PASTE CREDENTIAL INFO HERE
            、
            "label": "cloudantNoSQLDB"
        }
    }
}

使用您刚刚粘贴到编辑器中的凭证替换 PASTE CREDENTIAL INFO HERE。应包括花括号,以确保 JSON 对象的格式正确。

将该文件作为 vcap-local.json 保存到 Unit-14/config 目录中。该文件应该类似于清单 2。(我对自己的凭证作了模糊处理。)

清单 2. vcap-local.json 示例

{
    "services": {
        "cloudantNoSQLDB": {
            "credentials": {
                "apikey": "YOUR_API_KEY_HERE",
                "host": "YOUR_USERNAME_HERE.cloudant.com",
                "iam_apikey_description": "Auto generated apikey during resource-key operation for Instance - crn:v1:bluemix:public:cloudantnosqldb:us-south:a/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx::",
                "iam_apikey_name": "auto-generated-apikey-xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager",
                "iam_serviceid_crn": "crn:v1:bluemix:public:iam-identity::a/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                "password": "YOUR_PASSWORD_HERE",
                "port": 443,
                "url": "https://YOUR_USERNAME_HERE:YOUR_PASSWORD_HERE@YOUR_HOST_HERE.cloudant.com",
                "username": "YOUR_USERNAME_HERE"
            },
            "label": "cloudantNoSQLDB"
        }
    }
}

5. 创建数据库

使用汉堡菜单浏览至 IBM Cloud 仪表板,然后选择您的 Cloudant 服务。选择 Launch Cloudant Dashboard,您将看到如图 6 所示的仪表板。

图 6. Cloudant 数据库仪表板
图 6. Cloudant 数据库仪表板

图 6. Cloudant 数据库仪表板

单击图 6 的左侧菜单中的数据库图标,然后选择 Create Database。将该数据库命名为"shopping_list",然后单击 Create。您会看到如图 7 所示的屏幕。

图 7. 新创建的 shopping_list 数据库
图 7. 新创建的 shopping_list 数据库

图 7. 新创建的 shopping_list 数据库

现在,您已准备好将应用程序数据加载到数据库中。

6. 将应用程序数据加载到 Cloudant 中

我在自己的 GitHub 存储库中提供了一个名为 load-db-cloudant.js 的数据加载程序,您可以使用 npm run load-db 脚本来运行该程序。输出如下所示:

Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-14 sperry$ npm run load-db

> unit-14@1.0.0 load-db /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-14
> node ./utils/load-cloudant

[2018-08-17T23:20:07.351] [INFO] default - 1534566007350:INFO: Connect success! Connected to DB: shopping_list
[2018-08-17T23:20:07.355] [INFO] default - 1534566007355:INFO: mainline(): Initializing Cloudant...Done.
[2018-08-17T23:20:07.359] [INFO] default - 1534566007359:INFO: mainline(): Script start at: 8/17/2018, 11:20:07 PM
[2018-08-17T23:20:07.359] [INFO] default - 1534566007359:INFO: mainline(): Loading data for item...
[2018-08-17T23:20:07.359] [INFO] default - 1534566007359:INFO: loadData(): Loading data files...
[2018-08-17T23:20:07.360] [INFO] default - 1534566007360:INFO: loadData():readableStream.on(open): Opened file: ../data/Grocery_UPC_Database.csv
[2018-08-17T23:20:07.389] [DEBUG] default - 1534566007389:DEBUG: Creating documents: 1-10000
[2018-08-17T23:20:10.506] [DEBUG] default - 1534566010506:DEBUG: Creating documents: 10001-20000
[2018-08-17T23:20:13.591] [DEBUG] default - 1534566013591:DEBUG: Creating documents: 20001-30000
[2018-08-17T23:20:16.693] [DEBUG] default - 1534566016693:DEBUG: Creating documents: 30001-40000
[2018-08-17T23:20:19.804] [DEBUG] default - 1534566019804:DEBUG: Creating documents: 40001-50000
[2018-08-17T23:20:22.902] [DEBUG] default - 1534566022902:DEBUG: Creating documents: 50001-60000
[2018-08-17T23:20:26.009] [DEBUG] default - 1534566026009:DEBUG: Creating documents: 60001-70000
[2018-08-17T23:20:29.136] [DEBUG] default - 1534566029136:DEBUG: Creating documents: 70001-80000
[2018-08-17T23:20:32.253] [DEBUG] default - 1534566032253:DEBUG: Creating documents: 80001-90000
[2018-08-17T23:20:35.335] [DEBUG] default - 1534566035335:DEBUG: Creating documents: 90001-100000
[2018-08-17T23:20:38.433] [DEBUG] default - 1534566038433:DEBUG: Creating documents: 100001-110000
[2018-08-17T23:20:41.511] [INFO] default - 1534566041511:INFO: loadData():readableStream.on(close): Closed file: ../data/Grocery_UPC_Database.csv
[2018-08-17T23:20:41.511] [DEBUG] default - 1534566041511:DEBUG: Creating documents: 110001-110436
[2018-08-17T23:20:44.515] [INFO] default - 1534566044515:INFO: mainline(): Loading item data, done.
[2018-08-17T23:20:44.515] [INFO] default - 1534566044515:INFO: Total item documents loaded: 110436
[2018-08-17T23:20:44.515] [INFO] default - 1534566044515:INFO: mainline(): Script finished at: 8/17/2018, 11:20:44 PM
Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-14 sperry$

7. 验证是否已加载数据

shopping_list 数据库包含 110,436 个项。您应该会在 shopping_list 仪表板的 # of Docs 中看到此数字,如图 8 所示。

图 8. 成功运行 load-db-cloudant 数据加载程序后的 shopping_list 数据库
图 8. 成功运行 load-db-cloudant 数据加载程序后的 shopping_list 数据库

图 8. 成功运行 load-db-cloudant 数据加载程序后的 shopping_list 数据库

要查询数据,请选择 shopping_list 数据库,然后单击左侧菜单中的 Query。使用以下查询替换查询窗口的内容。

清单 3. 用于执行正则表达式搜索的 Cloudant 查询 JSON 格式

{
   "selector": {
      "itemDescription": {
         "$regex": ".*Free Range.*"
      }
   },
   "fields": [
      "_id",
      "_rev",
      "itemDescription"
   ]
}

此查询正在寻找其 itemDescription 属性包含短语"Free Range."的文档。 此查询应生成 15 个结果,如图 9 所示。

图 9. Cloudant 查询结果
图 9. Cloudant 查询结果

图 9. Cloudant 查询结果

您已经设置了 Cloudant 数据库,验证了数据,并且一切正常。接下来,您将运行"购物清单"应用程序。

8. 运行“购物清单”应用程序

除了在云中托管之外,您在前几个单元中运行的"购物清单"应用程序与此"购物清单"应用程序之间的唯一区别是记录系统。UI 的外观是相同的。

要运行该应用程序,请转至终端窗口并运行 npm start,或运行 npm run start-dev 以使用 nodemon

将浏览器转至 http://localhost:3000 以访问该应用程序的主页。

Cloudant Node API

Node.js 的官方 Cloudant 库 @cloudant/cloudantApache CouchDB 的一个分支的包装器。该分支称为 cloudant-nano,由 IBM Cloudant 团队维护。

在本节中,我将介绍如何使用 Cloudant 的 Node API 调用与"购物清单"应用程序中的 DAO 模块进行交互。首先连接到 IBM Cloud 中的 Cloudant 服务。

连接到 Cloudant

要将"购物清单"应用程序与 Cloudant 相连,首先需要创建一个新的 Cloudant 对象,并向此对象传递清单 1 中的 vcap-local.json 文件内的凭证。然后,调用 use() 函数以告知驱动程序要使用的数据库:

清单 4. 使用 @cloudant/cloudant Node 驱动程序连接到 Cloudant(来自 ./utils/utils.js

const Cloudant = require('@cloudant/cloudant');

const vcap = require('../config/vcap-local.json');

function dbCloudantConnect() {
    return new Promise((resolve, reject) => {
        Cloudant({  // eslint-disable-line
            url: vcap.services.cloudantNoSQLDB.credentials.url
        }, ((err, cloudant) => {
            if (err) {
                logger.error('Connect failure: ' + err.message + ' for Cloudant DB: ' +
                    appSettings.cloudant_db_name);
                reject(err);
            } else {
                let db = cloudant.use(appSettings.cloudant_db_name);
                logger.info('Connect success! Connected to DB: ' + appSettings.cloudant_db_name);
                resolve(db);
            }
        }));
    });
}

dbCloudantConnect() 函数将返回一个承诺。如果履行了该承诺,那么它将包含所需的数据库对象,以便使用 API。如果驳回了该承诺,那么将记录一个非空的 Error 对象,并使用该对象来驳回该承诺。

我已经编写了 DAO 实现,以便使用 IIFE (Immediately Invoked Function Expression) 来调用 dbCloudantConnect()。在模块加载时执行此调用。

清单 5. 调用 dbCloudantConnect() 以连接到 Cloudant(来自 ./models/lists-dao-cloudant.js

let db;

// Initialize the DB when this module is loaded
(function getDbConnection() {
    logger.info('Initializing Cloudant connection...', 'items-dao-cloudant.getDbConnection()');
    utils.dbCloudantConnect().then((database) => {
        logger.info('Cloudant connection initialized.', 'items-dao-cloudant.getDbConnection()');
        db = database;
    }).catch((err) => {
        logger.error('Error while initializing DB: ' + err.message, 'items-dao-cloudant.getDbConnection()');
        throw err;
    });
})();

如果履行了该承诺,那么会将数据库对象传递给 then() 进行保存。该模块中的 DAO 函数将使用 db 对象进行 Cloudant Node API 函数调用。

如果 getDbConnection() 成功运行(即没有 throw 一个 Error),即表明可以使用该连接。

创建新的购物清单

要创建新的购物清单,可以调用 lists-dao-cloudant.js 中的 create 函数并传递购物清单的描述,如清单 6 所示。

清单 6. 调用create()以创建新的购物清单(来自./models/lists-dao-cloudant.js

function create(description) {
    return new Promise((resolve, reject) => {
        let listId = uuidv4();
        let whenCreated = Date.now();
        let list = {
            _id: listId,
            id: listId,
            type: 'shoppingList',
            items: [],
            description: description,
            whenCreated: whenCreated,
            whenUpdated: null
        };
        db.insert(list, (err, result) => {
            if (err) {
                logger.error('Error occurred: ' + err.message, 'create()');
                reject(err);
            } else {
                resolve({ data: { createdId: result.id, createdRevId: result.rev }, statusCode: 201 });
            }
        });
    });
}

首先,该函数会创建一个名为 list 的新 JSON 文档对象,用于表示购物清单。随后,该函数会填充 list 对象(包括 type),该对象会设置为 shoppingList 以区别于 item(回想一下, Cloudant 中没有集合的概念),然后调用 db.insert() 来创建文档。

创建文档后,调用者会收到一个 JSON 响应,其中包含新文档 _id(作为 createdId)和 _rev(作为 createdRevId)。

如果无法创建文档,那么将驳回该承诺。

读取购物清单

要通过 _id 属性来访存单个购物清单,可以调用 lists-dao-cloudant.js 中的 findById() 函数,并传递要检索的文档 _id 的值:

清单 7. 调用 findById() 以通过 _id 属性来检索单个购物清单(来自 ./models/lists-dao-cloudant.js

function findById(id) {
    return new Promise((resolve, reject) => {
        db.get(id, (err, document) => {
            if (err) {
                if (err.message == 'missing') {
                    logger.warn(`Document id ${id} does not exist.`, 'findById()');
                    resolve({ data: {}, statusCode: 404 });
                } else {
                    logger.error('Error occurred: ' + err.message, 'findById()');
                    reject(err);
                }
            } else {
                resolve({ data: JSON.stringify(document), statusCode: 200 });
            }
        });
    });
}

该函数会调用 db.get(),传递要访存的文档 _id。如果文档 _id 不存在,那么将返回一个空的 JSON 对象以及 HTTP 状态码 404 (NOT FOUND)。否则,将向调用者返回该文档。

更新购物清单

要更新购物清单,可以调用 lists-dao-cloudant.js 中的 update() 函数,传递购物清单的文档 _id 以及用于更新的新 description 值。

清单 8. 调用 update() 以更新单个购物清单(来自 ./models/lists-dao-cloudant.js

function update(id, description) {
    return new Promise((resolve, reject) => {
        // Retrieve the list (need the rev)
        findById(id).then((response) => {
            // Parse the stringified JSON
            let list = JSON.parse(response.data);
            // Update the description
            list.description = description;
            list.whenModified = Date.now();
            // Update the document in Cloudant
            db.insert(list, (err, response) => {
                if (err) {
                    logger.error('Error occurred: ' + err.message, 'update()');
                    reject(err);
                } else {
                    resolve({ data: { updatedId: response.id, updatedRevId: response.rev }, statusCode: 200 });
                }
            });
        }).catch((err) => {
            logger.error('Error occurred: ' + err.message, 'update()');
            reject(err);
        });
    });
}

要更新现有文档,就需要使用该文档的 _rev 值,因此该函数的第一个任务是从 Cloudant 中检索该文档。然后,它会设置新的 description,并将 whenModified 设置为当前的日期和时间。最后,它会调用 db.insert() 来更新该文档。(由于 Cloudant 会插入文档的新版本并删除原始版本,因此它并不存在"更新"功能。)

更新文档后,调用者会收到一个 JSON 响应,其中包含更新的文档 ID(updatedId) 和新版本(updatedRevId)。

如果在此期间发生错误,那么会记录该错误并驳回该承诺。

按描述搜索项

要按部分描述搜索项,可以调用 items-dao-cloudant.js 中的 findByDescription() 函数并传递部分描述值,如清单 9 所示。

清单 9. 调用 findByDescription() 以访存其 itemDescription 属性包含指定 partialDescription 参数的所有文档

function findByDescription(partialDescription) {
    return new Promise((resolve, reject) => {
        let search = `.*${partialDescription}.*`;
        db.find({
            'selector': {
                'itemDescription': {
                    '$regex': search
                }
            }
        }, (err, documents) => {
            if (err) {
                reject(err);
            } else {
                resolve({ data: JSON.stringify(documents.docs), statusCode: (documents.docs.length > 0) ? 200 : 404 });
            }
        });
    });
}

首先,该函数会创建一个正则表达式,其中包含指定的 partialDescription 参数作为其 search 变量。然后,它会调用 db.find() 来查找所有匹配的文档。

将向调用者发送所返回的文档。如果发生错误,那么将驳回该承诺。

上面快速介绍了用于连接到 Cloudant 数据库的 Node 客户端 API 调用。 可查看 Cloudant Query 文档,了解有关查询 Cloudant 数据库的更多信息。

结束语

本单元介绍了 Cloudant,这是 IBM Cloud 中以文档为导向的非关系型数据库。 我展示了如何在 IBM Cloud 中设置 Cloudant 服务,将 Cloudant 连接到"购物清单"示例应用程序,以及从 shopping_list 数据库加载并验证数据。

我还介绍了 Cloudant Node.js 驱动程序中的一些 API 调用,您可以使用这些 API 调用与"购物清单"应用程序中的 DAO 模块进行交互。

要了解有关 Cloudant 的更多信息,建议您使用本单元中的代码:在调试器中运行代码(来自第 13 单元),并利用这些代码了解 Node 和 Cloudant 如何协同工作。您还可以尝试使用这些代码并添加新的功能。例如,您可以尝试在我提供的 contains 搜索的基础上添加不同类型的 partialDescription 搜索。

另外,还可查看以下视频,其中展示了如何设置 Cloudant 服务,帮助您熟悉 Cloudant 仪表板,了解如何通过仪表板直接使用数据库。

视频

观看视频:学习 Node.js,第 14 单元:迁移到 Cloudant 数据库即服务
观看视频:学习 Node.js,第 14 单元:迁移到 Cloudant 数据库即服务

Node.js 学习路径小结

本单元是 Node.js 学习路径的最后一部分,它全面介绍了 Node.js 开发。如果您自始至终完成了本课程的学习,那么这会为您今后的培训奠定坚实的基础,帮助您成长为一名专业的 Node.js 开发者。

如果您想继续学习本课程,可先从"购物清单"应用程序着手。使用源代码可加深对 Node.js 应用程序中的代码和模式的了解;我可以保证,与初次开始学习本课程时相比,您现在肯定发现了许多当时未曾察觉的代码相关内容!您还可以使用 npm 来集成 Node 生态系统中的新工具和软件包。当然,您应该始终记录、调试和分析代码,以便获得最佳的功能和性能。

参考资源

本文翻译自:Learn Node.js, Unit 14: Node.js with Cloudant DBaaS(2019-03-26)


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=1066519
ArticleTitle=学习 Node.js,第 14 单元:迁移到 Cloudant 数据库即服务
publish-date=08272019