Skip to content

Latest commit

 

History

History
1131 lines (833 loc) · 55.4 KB

File metadata and controls

1131 lines (833 loc) · 55.4 KB

十、连接到数据存储

如果您正在用 Node.js 构建一个应用,您几乎不可避免地需要某种形式的数据存储。这可以是简单的内存存储或任何数量的数据存储解决方案。Node.js 社区为您在应用开发中可能遇到的几乎所有数据存储创建了许多驱动程序和连接桥。在本章中,您将研究如何使用 Node.js 来连接其中的许多组件,包括:

  • 关系型数据库
  • 搜寻配置不当的
  • 一种数据库系统
  • MongoDB
  • 数据库
  • 使用心得
  • 卡桑德拉

image本章重点介绍 Node.js 与这些数据库的通信,而不是每个数据库的安装和初始化。因为本书旨在关注 Node.js 以及它如何在各种用例以及所有这些数据库类型中运行,所以鼓励您找到适合您特定数据库需求的方法。

10-1.连接到 MySQL

问题

许多开发人员最初是通过 MySQL 接触数据库编程的。正因为如此,许多人也希望将这种熟悉感(或桥梁)从现有应用带到 Node.js 应用中。因此,您希望能够从 Node.js 代码中连接到 MySQL 数据库。

解决办法

当您开始将 MySQL 集成到您的应用中时,您必须决定您希望用作 MySQL 驱动程序的 Node.js 框架。如果您选择使用 npm 并搜索 mysql,您可能会看到 MySQL 包列在顶部或顶部附近。然后用$ npm install mysql 命令安装这个包。

一旦安装了这个包,就可以在 Node.js 应用中使用 MySQL 了。为了连接和执行查询,你可以使用一个类似于你在清单 10-1 中看到的模块。在这个例子中,您可以利用 MySQL 示例数据库 Sakila,您可以根据在http://dev.mysql.com/doc/sakila/en/sakila-installation.html找到的说明来安装它。

清单 10-1 。连接和查询 MySQL

/**
* mysql
*/

var mysql = require('mysql');

var connectionConfig = {
        host: 'localhost',
        user: 'root',
        password: '',
        database: 'sakila'
};

var connection = mysql.createConnection(connectionConfig);

connection.connect(function(err) {
        console.log('connection::connected');
});

connection.query('SELECT * FROM actor', function(err, rows, fields) {
  if (err) throw err;

  rows.forEach(function(row) {
        console.log(row.first_name, row.last_name);
  });
});

var actor = { first_name: 'Wil', last_name: 'Wheaton' };
connection.query('INSERT INTO actor SET ?', actor, function(err, results) {
        if (err) throw err;

        console.log(results);
});

connection.end(function(err) {
        console.log('connection::end');
});

它是如何工作的

使用 mysql 模块连接到 MySQL 从一个连接配置对象开始。您的解决方案中的连接对象只是提供您希望连接的主机、用户、密码和数据库。这些是基本的设置,但是还有其他选项可以在该对象上配置,如您在表 10-1 中所见。

表 10-1 。MySQL 的连接选项

[计]选项 描述
bigNumberStrings 当与 supportBigNumbers 一起使用时,大数字将由 JavaScript 中的字符串表示。默认值:False
字符集 命名要用于连接的字符集。默认值:UTF8_GENERAL_CI
数据库ˌ资料库 列出 MySQL 服务器上的数据库名称。
调试 使用 stdout 打印详细信息。默认值:False
旗帜 列出要使用的非默认连接标志。
圣体 提供您要连接的数据库服务器的主机名。默认值:本地主机
安全认证 允许连接到不安全的(旧的)服务器验证方法。默认值:False
多重陈述 允许每个查询有多个语句。这可能会导致 SQL 注入的袭击。默认值:False
密码 列出 MySQL 用户的密码。
港口 给出 MySQL 服务器实例所在机器的端口号。默认值:3306
查询格式 创建自定义查询函数。
套接字路径 提供 Unix 套接字的路径。这将导致主机和端口被忽略。
stringifyObjects 将字符串化对象,而不是转换它们的值。默认值:False
支持 BigNumbers 在列中使用 BIGINT 或 DECIMAL 时使用此选项。默认值:False
时区 列出本地日期的时区。默认值:本地
分配担任特定类型角色 将类型转换为本机 JavaScript 类型。默认值:真
用户 列出用于身份验证的 MySQL 用户。

一旦创建了连接对象,就可以实例化一个到 MySQL 服务器的新连接。这是通过调用mysql.createConnection(config)来完成的,然后它将实例化连接对象并向其传递ConnectionConfig()对象。

你可以在清单 10-2 中看到,连接对象将实际尝试在协议模块中创建连接,该模块执行必要的 MySQL 握手以连接到服务器。

清单 10-2 。在 MySQL 模块中连接

module.exports = Connection;
Util.inherits(Connection, EventEmitter);
function Connection(options) {
  EventEmitter.call(this);

  this.config = options.config;

  this._socket        = options.socket;
  this._protocol      = new Protocol({config: this.config, connection: this});
  this._connectCalled = false;
  this.state          = "disconnected";
}

现在您已经连接到 MySQL 服务器,您可以使用该连接进行查询。在该解决方案中,您能够执行两种不同类型的查询。

第一个查询是来自数据库中 actor 表的显式 select 语句。这只需要将查询正确地形成为connection.query方法的第一个参数的字符串。connection.query方法最多可以接受三个参数:sql、值和回调。如果 values 参数不存在,则通过检查它是否是一个函数来检测它,然后只有 SQL 被排队以在服务器上执行。一旦查询完成,回调将被返回。

在第二个查询中,传递一些您希望在数据库中设置的值。这些值是 JavaScript 对象的形式,它们被传递给“?”插入查询上的占位符。使用这种方法的一个好处是,mysql 模块会尝试安全地对您加载到数据库中的所有数据进行转义。它这样做是为了减轻 SQL 注入的攻击。mysql 模块中有一个转义矩阵,会对不同的值类型执行不同类型的转义(见表 10-2 )。

表 10-2 。逃离矩阵

值类型 它是如何转换的
数组 转向列表['a ',' b'] => 'a ',' b '
布尔代数学体系的 True' / 'false '字符串
缓冲 十六进制字符串
日期 ' YYYY-mm-dd HH:ii:ss '字符串
NaN/无穷大 因为 MySQL 没有将它们转换成
嵌套数组 分组列表[['a ',' b'],['c ',' d']] => ('a ',b '),(' c ',' d ')
民数记 没有人
目标 生成键-'值'对;嵌套对象变成字符串
用线串 安全逃脱
未定义/空

这只是一个使用 mysql 模块连接和执行 MySQL 查询的基本示例。你也可以使用其他方法。您可以对查询的响应进行流式处理,并绑定到事件,以便在返回某一行时对该行执行特定的操作,然后再继续处理下一行。这方面的一个例子如清单 10-3 所示。

清单 10-3 。流式传输一个查询

/**
* mysql
*/

var mysql = require('mysql');

var connectionConfig = {
        host: 'localhost',
        user: 'root',
        password: '’,
        database: 'sakila'
};

var connection = mysql.createConnection(connectionConfig);

connection.connect(function(err) {
        console.log('connection::connected');
});

var query = connection.query('SELECT * FROM actor');

query.on('error', function(err) {

        console.log(err);

}).on('fields', function(fields) {

        console.log(fields);

}).on('result', function(row) {
        connection.pause();
        console.log(row);
        connection.resume();
}).on('end', function(err) {
        console.log('connection::end');
});

在这里,您可以看到查询本身并没有改变;我们不是向查询方法传递回调,而是绑定到查询执行时发出的事件。因此,在解析字段时,会对它们进行处理。然后,对于每一行,在移动到下一条记录之前,处理该数据。这是通过使用 connection.pause()函数,然后执行您的动作,接着是connection.resume()方法来完成的。

当您使用 mysql 模块这样的框架时,在 Node.js 中连接和使用 MySQL 非常简单。如果 MySQL 是您选择的数据库,它不应该限制您选择 Node.js 作为数据访问服务器的能力。

10-2.连接到微软 SQL 服务器

问题

您希望将 Node.js 应用集成到 Microsoft SQL Server 实例中。

解决办法

就像 MySQL 一样,使用 Node.js 为 Microsoft SQL Server 寻找驱动程序有几种解决方案。其中最受欢迎的一个包是“乏味的”,以连接到 SQL Server 的表格数据流(TDS) 协议命名。您首先使用$ npm install tedious命令通过 npm 安装这个包。

然后,构建一组与 SQL Server 交互的模块。这个解决方案的第一部分,清单 10-4 ,利用 dravoid 创建一个到 SQL Server 实例的连接。第二部分,如清单 10-5 所示,是包含与 SQL Server 实例上的数据交互的模块。

image 注意 SQL Server 是微软的产品。因此,只有当您的服务器运行 Windows 和 SQL Server 时,以下实现才有效。

清单 10-4 。将连接到您的 SQL Server 实例

/*
* Using MS SQL
*/

var TDS = require('tedious'),
        Conn = TDS.Connection,
        aModel = require('./10-2-1.js');

var conn = new Conn({
                username: 'sa',
                password: 'pass',
        server: 'localhost',
        options: {
            database: 'Northwind',
            rowCollectionOnRequestCompletion: true
        });

function handleResult(err, res) {
        if (err) throw err;
        console.log(res);
}

conn.on('connect', function(err) {
        if (err) throw err;

        aModel.getByParameter(conn, 'parameter', handleResult);

        aModel.getByParameterSP(conn, 'parameter', handleResult);
});

清单 10-5 。正在查询微软 SQL 服务器

var TDS = require('tedious'),
        TYPES = TDS.TYPES,
        Request = TDS.Request;
var aModel = module.exports = {
        // Use vanilla SQL
        getByParameter: function(conn, parm, callback) {
                var q = 'select * from model (NOLOCK) where identifier = @parm';

                var req = new Request(q, function(err, rowcount, rows) {
                        callback( err, rows );
                });
                req.addParameter('parm', TYPES.UniqueIdentifierN, parm);

                conn.execSql(req);
        },
        // Use a Store Procedure
        getByParameterSP: function(conn, parm, callback) {
                var q = 'exec sp_getModelByParameter @parm';
                var req = new Request(q, function(err, rowcount, rows) {
                        callback( err, rows );
                });
                req.addParameter('parm', TYPES.UniqueIdentifierN, parm);

                conn.execSql(req);
        }
};

它是如何工作的

当您第一次使用繁琐的模块连接到 Microsoft SQL Server 时,首先需要创建一个连接。这是通过使用TDS.Connection对象并用配置对象实例化它来完成的。在您的解决方案中,要创建连接,您需要发送用户名、密码、服务器名和一组用于连接的选项。有许多选项可以传递给这个对象,如表 10-3 所示。

表 10-3 。TDS。连接配置

环境 描述
选项.取消超时 取消请求超时前的时间。默认值:5 秒
选项. connectTimeout 等待连接尝试超时的时间。默认值:15 秒
选项.加密凭证详细信息 对象,该对象将包含加密所需的任何凭据。默认值:空对象“{ 0 }”
选项.数据库 要连接的数据库的名称
选项.调试.数据 布尔值,表示是否发送关于数据包数据的调试信息。默认值:False
选项. debug.packet 布尔值,表示是否发送关于数据包的调试信息。默认值:False
选项. debug.payload 布尔值,表示是否发送有关数据包有效负载的调试信息。默认值:False
选项. debug.token 布尔值,表明是否发送有关流标记的调试信息。默认值:False
选项.加密 设置是否加密请求。默认值:False
选项。实例 Name 要连接的命名实例。
选项。isolationLevel 服务器上的隔离级别,或者服务器何时允许从另一个操作中看到数据。默认值:未提交读(这被称为“脏读”,或最低级别的隔离。给定的事务可以看到来自另一个事务的未提交的事务。)
选项. packetSize 发送到服务器和从服务器接收的数据包的大小限制。默认值:4 KB
选项.端口 要连接的端口。此选项与 options.instanceName 互斥。默认值:1433
选项. requestTimeout 给定请求超时前的时间。默认值:15 秒
选项. rowcollectionondone 一个布尔值,表示当发出“done”、“doneInProc”和“doneProc”事件时将接收行集合。默认值:False
options . rowcollectiononrequestcompletion 布尔值,为真时,将在请求回调中提供行集合。默认值:False
options.tdsVersion 连接要使用的 TDS 协议的版本。默认值:7_2
options.textsize 为文本数据类型设置任何列的最大宽度。默认:2147483647
。密码 与用户名关联的密码
。计算机 Web 服务器 您希望连接的服务器的名称或 IP 地址
。用户名 用于连接到 MS SQL Server 实例的用户名(注意:不支持 Windows 身份验证连接。)

一旦将这些选项传递给连接对象,即 Node.js EventEmitter ,就可以绑定到“连接”事件。有几种方法可以从连接中发出“连接”事件,如下所述:

  • 成功的连接
  • 登录失败
  • connectTimeout过去之后
  • 在连接过程中出现套接字错误后

一旦成功连接到 SQL Server,就可以调用包含您的请求的模块。TDS.Request是一个EventEmitter,它允许您通过普通的 T-SQL 字符串或存储过程来执行 SQL。该请求还接受回调,要么直接调用回调,要么将结果应用到'requestCompleted'事件。

正如许多 SQL Server 实现一样,您可以将参数传递给希望执行的 SQL。在您的解决方案的两个示例中(一个是 SQL 文本,一个是存储过程),您传递了一个命名参数。这个命名参数通过使用Request.addParameter()方法被添加到请求中。addParameter()方法最多接受四个参数:名称、类型、值和一个选项对象。添加参数时使用的类型可以是允许作为参数一部分的TDS.Types对象中的任何类型。它们是 Bit、TinyInt、SmallInt、Int、BigInt、Float、Real、SmallDateTime、DateTime、VarChar、Text、NVarChar、Null、UniqueIdentifier 和 UniqueIdentifierN。

一旦创建了请求对象,并添加了所需的参数,就可以通过调用connection.execSql(<Request>)来执行 SQL 语句,将请求传递给方法。当请求完成时,您的回调执行,您可以相应地处理结果和行。

现在,您已经了解了如何使用 Node.js 和繁琐的包来管理 TDS 连接,从而实现到 MS SQL Server 的连接。

10-3.通过 Node.js 使用 PostgreSQL】

问题

您将在数据库中使用 PostgreSQL ,并且需要在 Node.js 应用中利用它。

解决办法

有几个软件包可用于连接 PostgreSQL。这个解决方案将利用 node-postgres 模块,这是 PostgreSQL 的一个低级实现。清单 10-6 显示了一个简单的例子,连接到一个 PostgreSQL 实例,执行一个简单的查询,然后记录结果。

清单 10-6 。连接到 PostgreSQL 并执行查询

/**
* PostgreSQL
*/

var pg = require('pg');

var connectionString = 'tcp://postgres:pass@localhost/postgres';

var client = new pg.Client(connectionString);

client.connect(function(err) {
        if (err) throw err;

        client.query('SELECT EXTRACT(CENTURY FROM TIMESTAMP "2011-11-11 11:11:11")', function(err, result) {
                if (err)  throw err;

                console.log(result.rows[0]);

                client.end();
        });
});

它是如何工作的

这个解决方案从使用$ npm install pg安装 node-postgres 模块开始。然后可以将它添加到 Node.js 代码中。然后,通过实例化一个新的客户机来创建到 PostgreSQL 实例的连接。客户端构造器可以解析连接字符串参数,然后你可以创建一个连接,如清单 10-7 所示。

清单 10-7 。node-postgres 的客户端构造

var Client = function(config) {
  EventEmitter.call(this);

  this.connectionParameters = new ConnectionParameters(config);
  this.user = this.connectionParameters.user;
  this.database = this.connectionParameters.database;
  this.port = this.connectionParameters.port;
  this.host = this.connectionParameters.host;
  this.password = this.connectionParameters.password;

  var c = config || {};

  this.connection = c.connection || new Connection({
    stream: c.stream,
    ssl: c.ssl
  });
  this.queryQueue = [];
  this.binary = c.binary || defaults.binary;
  this.encoding = 'utf8';
  this.processID = null;
  this.secretKey = null;
  this.ssl = c.ssl || false;
};

一旦创建了这个连接,接下来就要执行一个查询。这是通过调用client.query()并传递一个 SQL 字符串作为第一个参数来完成的。第二个参数可以是应用于查询的一组值,就像你在 10-1 节看到的那样,也可以是回调函数。回调函数将传递两个参数、一个错误(如果存在)或查询结果。如您所见,结果将包含一个返回行的数组。一旦您处理了结果,您就可以通过调用client.end()来关闭客户端连接。那个。end()方法将通过connection.end()方法关闭连接。

您的示例使用明文 SQL 语句来执行 node-postgres。使用 node-postgres 执行查询还有另外两种方法:参数化和预处理语句。

参数化查询允许您向查询传递参数,例如'select description from products where name=$1', ['sandals']'。通过使用参数化查询,您可以针对 SQL 注入攻击提供更高级别的保护。它们的执行速度也比纯文本查询慢,因为在每次执行之前,这些语句都要准备好,然后再执行。

使用 node-postgres 可以执行的最后一种查询是预处理语句。其中一个将被准备一次,然后对于到 postgres 的每个会话连接,这个 SQL 查询的执行计划被缓存,这样如果它被执行多次,它将成为使用 node-postgres 执行 SQL 的最有效的方式。像参数化查询一样,预处理语句也为 SQL 注入攻击提供了类似的屏障。准备好的语句是通过将一个对象传递给具有名称、文本和 values 属性的查询方法来创建的。然后,您可以通过您为它们提供的名称来调用这些准备好的语句。

利用 node-postgres 允许您从 Node.js 应用中直接高效地与 PostgreSQL 进行交互。接下来的部分将脱离 Node.js 的传统 SQL 接口,您将开始研究几种用于连接 Node.js 的非 SQL 选项。

10-4.使用 Mongoose 连接到 MongoDB

问题

您希望能够在 Node.js 应用中利用 MongoDB 。为此,您选择与 Mongoose 集成。

解决办法

当您在 Node.js 应用中使用 MongoDB 时,有许多驱动程序可供您选择来连接到您的数据存储。然而,最广泛使用的解决方案可能是将您的 MongoDB 实例与 Mongoose 模块集成。用$ npm install mongoose安装后,您可以使用清单 10-8 中列出的连接方法创建一个到 MongoDB 的连接。

清单 10-8 。使用 Mongoose 连接到 MongoDB

/**
* Connecting to MongoDB with Mongoose
*/

var mongoose = require('mongoose');

// simple connection string
// mongoose.connect('mongodb://localhost/test');
mongoose.connect('mongodb://localhost/test', {
        db: { native_parser: false },
        server: { poolSize: 1 }
        // replset:  { rs_name : 'myReplicaSetName' },
        // user: 'username',
        // pass: 'password'
});

// using authentication
// mongoose.connect('mongodb://username:password@host/collection')

mongoose.connection.on('open', function() {
        console.log('huzzah! connection open');
});

mongoose.connection.on('connecting', function() {
        console.log('connecting');
});

mongoose.connection.on('connected', function() {
        console.log('connected');
});

mongoose.connection.on('reconnected', function() {
        console.log('reconnected');
});

mongoose.connection.on('disconnecting', function() {
        console.log('disconnecting');
});

mongoose.connection.on('disconnected', function() {
        console.log('disconnected');
});

mongoose.connection.on('error', function(error) {
        console.log('error', error);
});

mongoose.connection.on('close', function() {
        console.log('connection closed');
});

它是如何工作的

一般来说,连接到 MongoDB 并不复杂。它需要一个特定于 MongoDB 的统一资源标识符(uniform resource identifier,URI)方案,该方案将指向一个(或多个)可以托管您的 MongoDB 数据的服务器。在 Mongoose 中,使用了相同的 URI 模式,如清单 10-9 所示,增加了几个选项,如清单 10-8 所示。

清单 10-9 。MongoDB 连接字符串

mongodb://[username:password@]host[:port][[,host2[:port2]...[,hostN[:portN][/database][?options]

对于猫鼬,你用mongoose.connect(<uri>, <options>)的方法。您在 Mongoose 中设置的选项可以像表 10-4 中列出的任何选项一样进行设置。

表 10-4 。猫鼬连接选项 T3】

[计]选项 描述
。作家(author 的简写) 身份验证机制选项,包括要使用的机制的来源和类型。
。 传递给连接。数据库实例(例如,{native_parser: true}将使用本机二进制 JSON [BSON]解析)。
.莽哥 布尔值,表示为您的. mongos 使用高可用性选项。如果连接到多个 Mongoose 实例,则应设置为 true。
。及格 与用户名关联的密码。
。replset 这是要使用的副本集的名称,假设您要连接的 Mongoose 实例是副本集的成员。
。计算机 Web 服务器 传递给连接服务器实例(例如,{poolSize: 1}个池)。
。用户 用于身份验证的用户名。

connection 对象继承了 Node.js EventEmitter ,因此您可以从您的解决方案中看到,您可以使用 Mongoose 订阅许多事件。这些事件在表 10-5 中进行了概述和描述。

表 10-5 。猫鼬连接事件

事件 描述
'关闭' 在所有连接上执行 disconnected 和“onClose”后发出。
'已连接' 成功连接到数据库后发出。
'正在连接' 对连接执行 connection.open 或 connection.openSet 时发出。
'断开连接' 断开连接后发出。
'断开连接' 执行 connection.close()事件时发出。
'错误' 当错误发生时(即当 Mongo 实例被删除时)发出。
'完整设置' 当所有 Node 都连接时,在副本集中发出。
'打开' 一旦打开到 MongoDB 实例的连接就发出。
'重新连接' 在后续连接后发出。

这是使用 Mongoose 连接到 MongoDB 的基本场景和设置。在下一节中,您将研究如何使用 Mongoose 智能地建模数据存储并在 MongoDB 中检索它。

10-5.猫鼬的建模数据

问题

您希望在 Node.js 应用中使用 Mongoose 对 MongoDB 数据建模。

解决办法

用 Mongoose 建模数据时,需要利用mongoose.model()方法 。您不一定需要mongoose.Schema方法,但是对于在清单 10-10 中创建的模型,它被用来构建模型的模式。

清单 10-10 。用猫鼬创建模型

/**
* Modeling data with Mongoose
*/

var mongoose = require('mongoose'),
        Schema = mongoose.Schema,
        ObjectId = Schema.ObjectId;

mongoose.connect('mongodb://localhost/test');

var productModel = new Schema({
        productId: ObjectId,
        name: String,
        description: String,
        price: Number
});

var Product = mongoose.model('Product', productModel);

var sandal = new Product({name: 'sandal', description: 'something to wear', price: 12});

sandal.save(function(err) {
        if (err) console.log(err);

        console.log('sandal created');
});

Product.find({name: 'sandal'}).exec(function(err, product) {
        if (err) console.log(err);

        console.log(product);
});

它是如何工作的

对于在 Node.js 应用中使用 MongoDB 的人来说,mongose 成为高优先级的原因之一是因为 mongose 自然地对数据建模。这意味着您只需要为您的数据生成一个模式模型,然后就可以使用该模型从 MongoDB 获取、更新和删除数据。

在您的解决方案中,首先导入mongoose.Schema对象。模式是通过传递一个 JavaScript 对象来创建的,该对象包含您希望建模的实际模式,以及一个可选的第二个参数,该参数包含您的模型的选项。该模式不仅允许您在模型中创建字段的名称,还为您提供了为模式中的值命名特定类型的机会。模式实例化中实现的类型显示为{ <fieldname> : <DataType> }。可用类型如下:

  • 排列
  • 布尔缓冲区
  • 日期
  • 混合的
  • 数字
  • ObjectId(对象 Id)

字符串您为您的模式创建的选项是在表 10-6 中显示的任何选项。

表 10-6 。猫鼬的选择。计划

[计]选项 描述
自动索引 决定 MongoDB 是否自动生成索引的布尔值。默认值:真
buffer 命令 一个布尔值,它决定当连接丢失时命令是否被缓冲,直到重新连接发生。默认值:真
脱帽致意 将 MongoDB 设置为有上限——这意味着集合的大小是固定的。默认值:False
募捐 设置集合名称的字符串。
编号 返回文档的 _id 字段,或对象的十六进制字符串。如果设置为 false,这将是未定义的。默认值:真
_id 告知 MongoDB 是否会在创建模型对象时创建 _id 字段。默认值:真
阅读 在架构上设置 query.read 选项。这个字符串决定了您的应用是从复制集中的主要、辅助还是最近的 Mongo 读取。选项:'主要' '主要优先' '次要' '次要优先' '最近'
安全的 布尔值,它设置是否将错误传递给回调。默认值:真
分布式 设置以哪个分片集合为目标。
严格的 确保传递给构造函数的非模型值不被保存的布尔值。默认值:真
托杰森 将模型转换为 JavaScript 对象表示法(JSON)。
图征物件 将模型转换为普通的 JavaScript 对象。
版本密钥 创建模型时设置模式的版本。默认值:__v: 0

当您创建您的“产品”模式时,您创建了一个包含一个productId的简单对象,它导入产品的ObjectIdhexString。您的模型还将为您希望存储和检索的产品创建一个字符串形式的名称、一个字符串形式的描述和一个数字形式的价格。

从 schema 对象中,您现在实际上创建了一个 Mongoose 模型,方法是使用mongoose.model()方法并传递您为模型选择的名称和 schema 模型本身。现在,您可以使用这个新产品模型来创建产品。您可以通过在 MongoDB 服务器上传递您希望在文档中建模的对象来做到这一点。在此解决方案中,您将创建一个凉鞋对象。然后通过使用接受回调的sandal.save()方法来保存它。

您还可以从您的模型中查找和删除数据。在这个解决方案中,您使用Product.find({ name: 'sandal' }),查询您的模型,它将搜索所有名为“sandal”的产品,并在 exec()回调中返回这些产品。从回调中,您可以访问名为“sandal”的所有产品的数组。如果您希望删除全部或部分结果,您可以遍历这些结果并逐个删除它们,如清单 10-11 所示。

清单 10-11 。使用猫鼬删除记录

Product.find({name: 'sandal'}).exec(function(err, products) {
        if (err) console.log(err);
        console.log(products);
        for (var i = 0; i < products.length; i++) {
                if (i >= 3) {
                        products[i].remove(function() {
                                console.log('removing');
                        });
                }
        }
});

您已经看到了如何在 Node.js 应用中使用 Mongoose 连接和实现一个模式。Mongoose 对象模型允许将您的模型干净地实现到 MongoDB 文档数据库。

10-6.连接到 CouchDB

问题

您希望在 Node.js 应用中利用 CouchDB 。

解决办法

CouchDB 是一个数据库,它利用 JSON 文档、HTTP 的应用编程接口(API)和 JavaScript 的 MapReduce。正因为如此,成为了很多 Node.js 开发者的天然契合点。有几个模块可用于使用 CouchDB 构建 Node.js 应用。在这个解决方案中,您将利用 Nano,这是一个支持 CouchDB 的轻量级模块。可以使用$ npm install nano 进行安装。

在清单 10-12 中,您将创建一个数据库并将一个文档插入到该数据库中。接下来,您将更新数据库中文档。然后,您将在清单 10-13 中检索文档并将其从数据库中删除。

清单 10-12 。用 Nano 在 CouchDB 中创建数据库和文档

/**
* CouchDB
*/

var nano = require('nano')('http://localhost:5984');

nano.db.create('products', function(err, body, header) {
        if (err) console.log(err);

        console.log(body, header);
});

var products = nano.db.use('products', function(err, body, header) {
        if (err) console.log(err);

        console.log(body, header);
});

products.insert({ name: 'sandals', description: 'for your feet', price: 12.00}, 'sandals', function(err, body, header) {
        if (err) console.log(err);

        console.log(body, header);
});

products.get('sandals', {ref_info: true}, function(err, body, header) {
        if (err) console.log(err);

        console.log(body, header);
});

// Updating in couchDB with Nano.
products.get('sandals', function(err, body, header) {
        if (!err) {
                products.insert({name: 'sandals', description: 'flip flops', price: 12.50, _rev: body._rev }, 'sandals', function(err, body, header) {
                        if (!err) {
                                console.log(body, header);
                        }
                });
        }
});

清单 10-13 。从 CouchDB 数据库中删除文档

var nano = require('nano')('http://localhost:5984');

var products = nano.db.use('products');
// deleting in couchDB with Nano.
products.get('sandals', function(err, body, header) {
        if (!err) {
                products.destroy( 'sandals', body._rev, function(err, body, header) {
                        if (!err) {
                                console.log(body, header);
                        }
                        nano.db.destroy('products');
                });
        }
});

您还可以通过一个单一的nano.request()接口使用 Nano 创建对 CouchDB 的请求,如清单 10-14 所示。

清单 10-14 。使用 nano.request()

nano.request({
        db: 'products',
        doc: 'sandals',
        method: 'get'
        }, function(err, body, header) {
                if (err) console.log('request::err', err);
                console.log('request::body', body);
        });

它是如何工作的

使用 CouchDB 是许多 Node.js 应用的天然选择。Nano 模块被设计为“Node.js 的极简 CouchDB 驱动程序”,不仅是 Nano 极简,而且它还支持使用管道,您可以从 CouchDB 直接访问错误。

清单 10-12 中,您首先需要 Nano 模块并连接到您的服务器。连接到服务器就像指向包含正在运行的 CouchDB 实例的主机和端口一样简单。接下来,在 CouchDB 服务器上创建一个数据库,它将保存您希望创建的所有产品。当您使用 Nano 调用任何方法时,您可以添加一个回调来接收错误、主体和头参数。当你第一次用 Nano 创建数据库时,你会看到主体和请求响应 JSON,看起来像清单 10-15

清单 10-15 。创建“产品后回调

Body: { ok: true }
Header: { location: 'http://localhost:5984/products',
  date: 'Sun, 28 Jul 2013 14:34:01 GMT',
  'content-type': 'application/json',
  'cache-control': 'must-revalidate',
  'status-code': 201,
  uri: 'http://localhost:5984/products' }

一旦创建了数据库,就可以创建第一个产品文档。通过将包含产品信息的 JavaScript 对象传递给products.insert() 方法,可以创建“sandals”文档。第二个参数是您希望与该文档相关联的名称。正文和标题的响应会让你知道插入是正确的,如清单 10-16 中的所示。

清单 10-16 。插入产品

Body: { ok: true,
  id: 'sandals',
  rev: '1-e62b89a561374872bab560cef58d1d61' }
Header: { location: 'http://localhost:5984/products/sandals',
  etag: '"1-e62b89a561374872bab560cef58d1d61"',
  date: 'Sun, 28 Jul 2013 14:34:01 GMT',
  'content-type': 'application/json',
  'cache-control': 'must-revalidate',
  'status-code': 201,
  uri: 'http://localhost:5984/products/sandals' }

如果您想用 Nano 更新 CouchDB 中的一个文档,您需要获得您希望更新的特定文档的修订标识符,然后再次调用nano.insert()函数,将修订标识符传递给您希望更新的特定文档。在您的解决方案中,您通过使用nano.get()方法,然后使用来自回调的 body._rev修订标识符来更新文档(参见清单 10-17 )。

清单 10-17 。更新现有文档

products.get('sandals', function(err, body, header) {
        if (!err) {
                products.insert({name: 'sandals', description: 'flip flops', price: 12.50, _rev: body._rev
}, 'sandals', function(err, body, header) {
                        if (!err) {
                                console.log(body, header);
                        }
                });
        }
});

创建、插入和更新文档后,您可能希望能够不时地删除项目。为此,您还需要一个对您计划删除的文档修订的引用。这意味着您可以首先用nano.get()获取文档,并使用来自回调的body._rev标识符传递给nano.destroy()方法。这将从数据库中删除该文档。但是,如果您想删除您的数据库,您可以通过调用nano.db.destroy(<DBNAME>);来销毁整个数据库。

Nano 框架的关键在于所有这些函数实际上都是你在清单 10-14 中看到的nano.request()方法的包装器。在清单 10-14 中,您的请求是以 HTTP GET 的形式向“产品”数据库中的“凉鞋”文档发出的。这个和nano.get()一样。销毁操作的等效操作是使用 HTTP 动词 DELETE,因此在您的nano.db.destroy('products')的实例中,您实际上是在编写nano.request({db: 'products', method: 'DELETE'}, callback);.

10 比 7。使用 Redis

问题

您希望在 Node.js 应用中利用 Redis 键值存储。

解决办法

Redis 是一个非常强大和流行的键值数据存储。因为它太受欢迎了,所以 Node.js 有很多实现可供选择。一些被用来连接到 Express web 服务器框架,而其他的只是指定的。Redis 网站上推荐的一个实现是 node_redis,位于https://github.com/mranney/node_redis。要安装 node_redis,可以按如下方式利用 NPM:$ npm install redis

对于熟悉 redis 的人来说,使用 redis_node 很简单,因为 API 是相同的。您的所有 get、set、hget 和 hgetall 命令都可以直接从 Redis 本身执行。清单 10-18 中显示了一个获取和设置值和哈希值的简单示例。

清单 10-18 。用 node_redis 获取和设置字符串和散列键值对

/**
* Redis
*/

var redis = require("redis"),
    client = redis.createClient();

client.on("error", function (err) {
    console.log("Error " + err);
});

client.set("key", "value", redis.print);

client.hset("hash key", "hashtest 1", "some value", redis.print);
client.hset(["hash key", "hashtest 2", "some other value"], redis.print);
client.hkeys("hash key", function (err, replies) {
    console.log(replies.length + " replies:");
    replies.forEach(function (reply, i) {
        console.log("    " + i + ": " + reply);
    });
    client.quit();
});

client.hgetall('hash key', function(err, replies) {
    replies.forEach(function(reply) {
        console.log(reply);
    });
});

client.get("key", function(err, reply) {
    if (err) console.log(err);

    console.log(reply);
});

其他时候,您可能希望实现一个松散耦合的发布和订阅范例,而不是仅仅为会话级的键值存储存储散列。对于许多 Node.js 应用的开发人员来说,这可能是一种非常熟悉的方法,他们已经熟悉了事件驱动的开发,但是希望利用 Redis 来实现这些目的。清单 10-19 中显示了一个使用发布和订阅的例子。

清单 10-19 。发布和订阅示例

/**
* Pub/Sub

*/

var redis = require("redis"),
    subscriber = redis.createClient(),
    publisher = redis.createClient();

subscriber.on("subscribe", function (topic, count) {
    publisher.publish("event topic", "your event has occured");
});

subscriber.on("message", function (topic, message) {
    console.log("message recieved:: " + topic + ": " + message);
    subscriber.end();
    publisher.end();
});

subscriber.subscribe("event topic");

它是如何工作的

一旦你通过$ npm install redis安装了 node_redis,你就可以访问 Node.js 中 redis 的完整实现。正如你在清单 10-18 中看到的,你可以很容易地利用redis.createClient()创建一个新的客户端。createClient()方法将创建一个到 Redis 实例的端口和主机的连接,默认为http://127.0.0.1:6379,然后将实例化一个 RedisClient 对象,如清单 10-20 所示。

清单 10-20 。Node_redis 创建客户端

exports.createClient = function (port_arg, host_arg, options) {
    var port = port_arg || default_port,
        host = host_arg || default_host,
        redis_client, net_client;

    net_client = net.createConnection(port, host);

    redis_client = new RedisClient(net_client, options);

    redis_client.port = port;
    redis_client.host = host;

    return redis_client;
};

RedisClient 继承了 Node.js EventEmitter,会发出几个事件,如 表 10-7 所示。

表 10-7 。再贴现事件

事件 描述
'连接' 此事件将与“就绪”同时发出,除非客户端选项“no_ready_check”设置为 true,在这种情况下,只有在建立连接后才会发出此事件。然后你就可以自由地向 Redis 发送命令了。
'排水' 当到 Redis 服务器的传输控制协议(TCP)连接已经缓冲但再次可写时,RedisClient 将发出“drain”。
'结束' 一旦到 Redis 服务器的客户端连接关闭,就会发出此事件。
'错误' 当 Redis 服务器出现异常时,RedisClient 将发出“error”。
“闲置” 一旦没有等待响应的未完成消息,RedisClient 将发出“idle”。
准备好了吗 一旦建立了到 Redis 服务器的连接,客户端将发出“就绪”事件,服务器报告它已准备好接收命令。如果您在“就绪”事件之前发送命令,它们将在该事件发出之前排队并执行。

在您的解决方案中,然后设置一个字符串值和一个哈希值。使用client.setclient.get设置和检索字符串值。为了处理散列,您还使用了client.hsetclient.hkeysclient.hgetall。这些方法直接等同于直接输入命令(见清单 10-21 )。

清单 10-21 。雷迪斯集、get、hset、hkeys 和 hgetall

> set key value
OK
> get key
"value"
> hset 'hash key' 'hashtest 1' 'blah'
(integer) 0
> hset 'hash key' 'hashtest 2' 'cheese'
(integer) 0
> hkeys 'hash key'
1) "hashtest 1"
2) "hashtest 2"
> hgetall 'hash key'
1) "hashtest 1"
2) "blah"
3) "hashtest 2"
4) "cheese"

然后,您创建了一个发布和订阅解决方案。这可以与 Node.js 的事件模型一起使用,以便在应用的隔离部分之间创建一个松散耦合的集成。首先,您创建了两个名为 publisher 和 subscriber 的RedisClients。首先,在您希望收听的主题上调用subscriber.subscribe(),然后一旦订户的‘subscribe’事件被发出,就使用publisher.publish(<event name>)实际发出该事件。然后,您可以将订阅者绑定到消息事件,并在该事件发布后执行各种操作。

现在,您已经利用 Redis 存储了键-值对,以及带有 node_redis 的数据存储中的散列键。您还使用 Redis 执行了发布和订阅方法来支持这些消息。

10-8.连接到卡珊德拉

问题

您正在利用 Cassandra 来记录来自您的应用的事件,并且您希望用 Node.js 来实现这种日志记录。

解决办法

在不同的编程语言中,Cassandra 有许多不同的驱动程序。对于 Node.js,与$ npm install helenus一起安装的包“helenus”处于最前沿,因为它提供了对 thrift 协议和 Cassandra 查询语言(CQL) 的绑定。

清单 10-22 中,您将创建一个日志机制来记录 Node.js 服务器上发生的事件。

清单 10-22 。使用 helenus 为 Cassandra 创建一个日志应用

var helenus = require('helenus'),
  pool = new helenus.ConnectionPool({
     hosts      : ['127.0.0.1:9160'],
     keyspace   : 'my_ks',
     user       : 'username',
     password   : 'pass',
     timeout    : 3000//,
     //cqlVersion : '3.0.0' // specify this if you're using Cassandra 1.1 and want to use CQL 3
});

var logger = module.exports = {
    /**
    * Logs data to the Cassandra cluster
    *
    * @param status     the status event that you want to log
    * @param message    the detailed message of the event
    * @param stack      the stack trace of the event
    * @param callback   optional callback
    */
    log: function(status, message, stack, callback) {
        pool.connect(function(err, keyspace){
            console.log('connected');
            keyspace.get('logger', function(err, cf) {
                var dt = Date.parse(new Date());
                //Create a column
                var column = {};
                column['time'] = dt;
                column['status'] = status;
                column['message'] = message;
                column['stack'] = stack;

                var timeUUID = helenus.TimeUUID.fromTimestamp(new Date());

                cf.insert(timeUUID, column, function(err) {
                    if (err) {
                        console.log('error', err);
                    }
                    Console.log('insert complete');
                    if (callback) {
                        callback();
                    } else {
                        return;
                    }

                });
            });
        });
    }
};

它是如何工作的

helenus 模块是连接到 Cassandra 数据库的健壮解决方案。在您的解决方案中,导入 helenus 模块后,您连接到 Cassandra。这是通过向helenus.connectionPool()传递一个简单的对象来实现的。创建这个连接池的对象包含几个选项,如 表 10-8 所示。

表 10-8 。连接池选项

[计]选项 描述
。cqlVersion 命名您希望使用的 CQL 版本。
。主机 提供一个值数组,这些值是群集中所有 Cassandra 实例的 IP 地址和端口。
。keyspace(键空间) 列出您希望最初连接到的 Cassandra 集群上的密钥空间。
。密码 提供您希望用来连接到 Node 的密码。
。超时 超时时间,以毫秒为单位。
。用户 给出连接到 Node 的用户名。

一旦建立了连接,您就可以调用pool.connect()。一旦连接发生,回调将提供对您在连接池中配置的默认键空间的引用。然而,有另一种方法可以通过使用pool.use('keyspacename', function(err, keyspace) {});方法连接到一个键空间。

现在,您可以访问 Cassandra 集群上的密钥空间。要访问 logger 列族,您可以调用keyspace.get('logger'...),它将获取列族并返回一个引用,这样您就可以直接对列族进行操作。

现在您已经获得了对希望写入数据的列族的访问权,您可以创建想要插入的列了。在这个解决方案中,假设您的 logger 列族有一个 TimeUUID 类型的行键,为每个条目创建一个惟一的时间戳。 Helenus 允许您轻松使用这种类型的键,因为 TimeUUID 是一种内置类型。您可以访问这个类型,并通过在对象上使用fromTimestamp方法创建一个新的 TimeUUID,如清单 10-23 中的所示。您还将看到,如果需要,helenus 提供了一种生成 UUID 类型的方法。

清单 10-23 。创建新的 TimeUUID

 > helenus.TimeUUID.fromTimestamp(new Date());
c19515c0-f7c4-11e2-9257-fd79518d2700

> new helenus.UUID();
7b451d58-548f-4602-a26e-2ecc78bae57c

除了 logger 列族中的行键之外,您只需传递希望记录的事件的时间戳、状态、消息和堆栈跟踪。这些都成为您命名为“列”的对象的一部分现在已经有了行键和列值,可以通过调用列族上的cf.insert方法将它们插入到 Cassandra 中。

这个解决方案利用 JavaScript 和对象生成一个类似模型的实现,该实现被转换成 Cassandra Thrift 协议,以便插入数据。Helenus 允许通过使用 CQL 语言插入数据的其他方法。与清单 10-22 中的类似的实现,但是使用了 CQL ,如清单 10-24 中的所示。检索列族的步骤被省略了,因为 CQL 直接在键空间上操作。

清单 10-24 。使用 CQL 将数据记录到卡珊德拉

var helenus = require('helenus'),
    pool = new helenus.ConnectionPool({
       hosts      : ['127.0.0.1:9160'],
       keyspace   : 'my_ks',
       user       : 'username',
       password   : 'pass',
       timeout    : 3000//,
       //cqlVersion : '3.0.0' // specify this if you're using Cassandra 1.1 and want to use CQL 3
    });

var logger = module.exports = {
    /**
    * Logs data to the Cassandra cluster
    *
    * @param status     the status event that you want to log
    * @param message    the detailed message of the event
    * @param stack      the stack trace of the event
    * @param callback   optional callback
    */
    log: function(status, message, stack, callback) {
        pool.connect(function(err, keyspace){
            keyspace.get('logger', function(err, cf) {
                var dt = Date.parse(new Date());
                //Create a column
                var column = {};
                column['time'] = dt;
                column['status'] = status;
                column['message'] = message;
                column['stack'] = stack;

                var timeUUID = helenus.TimeUUID.fromTimestamp(new Date());
                var cqlInsert = 'INSERT INTO logger (log_time, time, status, message,stack)' +
                                'VALUES ( %s, %s, %s, %s, %s )';

                var cqlParams = [ timeUUID, column.time, column.status, column.message, column.stack ];
                pool.cql(cqlInsert, cqlParams, function(err, results) {
                    if (err) logger.log('ERROR', JSON.stringify(err), err.stack);
                });
            });
        });
    }
};

var queueObj = {};
var timeUUID = helenus.TimeUUID.fromTimestamp(new Date()) + '';
                    var cqlInsert = 'INSERT INTO hx_services_pha_card (card_id, card_definition_id, pig_query, display_template, tokens, trigger)' +
                                    'VALUES ( %s, %s, %s, %s, %s, %s )';

                    var cqlParams = [ timeUUID, queueObj.card_definition_id, queueObj.pig_query, queueObj.display_template, tokens.join(','), queueObj.trigger ];
                    pool.cql(cqlInsert, cqlParams, function(err, results) {
                        if (err) logger.log('ERROR', JSON.stringify(err), err.stack);
                    });

10-9.对 Node.js 使用 Riak

问题

您希望能够在 Node.js 应用中利用高度可伸缩的分布式数据库 Riak。

解决办法

Riak 是为分布式系统的高可用性而设计的。它被设计为快速和可伸缩的,这使得它非常适合许多 Node.js 应用。在清单 10-25 中,您将再次创建一个数据存储,它将创建、更新和检索您的产品数据。

image 注意 Riak 目前在 Windows 机器上不支持。下面的实现应该可以在 Linux 或 OSX 上工作。

清单 10-25 。通过 Node.js 使用 Riak

/**
* RIAK
*/

var db = require('riak-js').getClient();

db.exists('products', 'huaraches', function(err, exists, meta) {
        if (exists) {
                db.remove('products', 'huaraches', function(err, value, meta) {
                        if (err) console.log(err);
                        console.log('removed huaraches');
                });
        }
});

db.save('products', 'flops', { name: 'flip flops', description: 'super for your feet', price: 12.50}, function(err) {
        if (err) console.log(err);
        console.log('flip flops created');
        process.emit('prod');
});

db.save('products', 'flops', { name: 'flip flops', description: 'fun for your feet', price: 12.00}, function(err) {
        if (err) console.log(err);
        console.log('flip flops created');
        process.emit('prod');
});

db.save('products', 'huaraches', {name: 'huaraches', description: 'more fun for your feet', price: 20.00}, function(err) {
        if (err) console.log(err);

        console.log('huaraches created');
        process.emit('prod');

        db.get('products', 'huaraches', function(err, value, meta) {
                if (err) console.log(err);
                console.log(value);
        });

});

process.on('prod', function() {

        db.getAll('products', function(err, value, meta) {
                if (err) console.log(err);
                console.log(value);
        });

});

它是如何工作的

为了创建这个解决方案,您从使用$ npm install riak-js安装的 Node.js 模块 riak-js 开始。然后你通过使用getClient()方法连接到服务器。这种方法在不使用的情况下,会发现默认的客户端运行在本地机器上,但是也可以用 options 对象进行配置。

现在,您通过使用 db 对象连接到了 Riak 实例。首先,当你遇到db.exists(<bucket>, <key>, callback)时,你看到 API 是简洁的。如果这个键存在于 Riak Node 上的 bucket 中,这个回调将返回 true 值。如果存储桶键确实存在,您只需指向该存储桶键并使用db.remove()方法就可以删除该特定数据集。

接下来,使用db.save方法将一些数据保存到 Riak Node。该方法接受一个桶、一个键和一个您希望为桶键设置的值。这可以是一个 JavaScript 对象、一个数字或一个您希望为键值存储的字符串。与 riak-js 的所有请求一样,您也可以访问回调函数。回调有三个值:一个错误(如果发生的话)、一个作为 riak-js 方法的结果传递的值和一个元对象。

在您将 huaraches 保存到产品存储桶之后,您可以通过使用db.get()功能来检索这个密钥。同样,这个方法使用桶和键来确定您希望检索 Node 上的哪些数据。回调可以包含与数据相关联的值和元。使用 riak-js 还有一种方法可以访问数据。这用于检索给定存储桶的所有值。回调中的结果值将是一个与桶相关联的数据数组。

您已经使用 riak-js 与 Node.js 中的 riak 集群进行了交互。Riak 是一个强大的分布式数据库解决方案,除了这些简单的任务之外,还可以通过类似的 API 使用 map 和 reduce 函数执行更复杂的搜索。为此,您可以通过运行以下命令来搜索您的 Node 中的所有产品(参见清单 10-26 )。

清单 10-26 。使用 riak-js 减少地图

db.mapreduce
        .add('products')
        .map(function(v) {
                return [Riak.mapValuesJson(v)[0]];
        })
        .run(function(err, value, meta) {
                console.log(value);
        });