每个应用迟早都需要存储来保存其数据。Node.js 有几个包可以与几乎每个数据库通信。在本章中,我们将看到使用 PostgreSQL 和 MongoDB 的一些基本操作,这两个数据库在它们自己的类别中使用最多。
PostgreSQL 是一个关系型开源数据库,几乎可以在所有环境中使用。要将 PostgreSQL 与 Node.js 一起使用,有几个可用的包,但使用最多的是基本驱动程序pg
。
使用npm
安装驱动程序后,我们可以使用以下代码连接到数据库:
代码清单 57
const pg =
require('pg')
const
conString = "postgres://usr:pwd@serverName/databaseName"
pg.connect(conString,
(err, client) => {
// Do
something with client here
})
语法非常简单易懂。现在我们有了一个客户端连接到数据库的实例,我们可以发出一个查询来提取一些数据。
对于以下示例,我们将考虑这样一个表:
表 2:样本数据
| 样表:电影 | | id | 标题 | 年 | | one | 她 | Two thousand and thirteen | | Two | 我,机器人 | Two thousand and four | | three | 人工智能 | Two thousand and one | | four | 2001:太空漫游 | One thousand nine hundred and sixty-eight | | five | 叶片转轮 | One thousand nine hundred and eighty-two |
代码清单 58
const pg =
require('pg')
const
conString = "postgres://usr:pwd@serverName/databaseName"
pg.connect(conString,
(err, client) => {
client.query("SELECT
* FROM movies", (err, res) => {
// res.rows contains the record array
})
})
这段代码对电影运行查询,提取表中的所有记录。记录在结果对象的rows
属性中可用,它是一个我们可以用来访问数据的数组。
如果我们需要指定一个特定的参数,我们可以运行参数化查询。例如,假设我们需要提取 1982 年发行的所有电影。我们只需添加一个带有参数的where
子句:
代码清单 59
const pg =
require('pg')
const
conString = "postgres://usr:pwd@serverName/databaseName"
const year
= 1982
pg.connect(conString,
(err, client) => {
client.query({text:"SELECT
* FROM movies WHERE year = $1", values: [year]}}, (err, res) => {
// do something with data
})
})
在这种情况下,我们将带有text
属性的对象(查询文本)传递给query
,其中占位符$1
标记了第一个参数的位置。参数值在values
数组中(已排序),驱动程序将检查脏字符串以避免 SQL 注入。
node-pg
驱动程序支持基于事件的应用编程接口,用于消费SELECT
语句的结果。client
对象有两个事件:row
和end
。从数据库中提取的每一个新行都会引发row
事件,而end
事件在没有更多行可用时到达:
代码清单 60
const pg = require('pg')
const conString =
"postgres://usr:pwd@serverName/databaseName"
const movies = []
const query = client.query('select
title, year from movies')
query.on('row', function(row,
result) {
movies.push(row)
});
query.on('end', function(result) {
console.log(movies.length + ' movies
were load');
});
query
对象的使用在这里有一点不同:使用client.query
函数,我们获得一个可以附加一些事件的对象。query
物体基本上是一个事件发射器。
在row
事件上,我们简单地将行收集到一个数组中,在end
事件上,我们将消息打印到控制台。
我们应该使用哪个应用编程接口取决于我们需要使用数据的上下文。第一种方法更适合于在做其他事情之前需要所有数据的情况。后者是基于事件的,更可取的是,您可以返回部分结果,并在它们到达时立即开始使用它们。
所以查询数据库相当容易。但是其他 CRUD 操作呢?
模式是相同的:我们需要创建一个查询(INSERT、UPDATE 或 DELETE)并传递具有正确值的参数。
例如,假设我们想在列表中添加一部新电影:
代码清单 61
const pg =
require('pg')
const
conString = "postgres://usr:pwd@serverName/databaseName"
const year
= 1982
pg.connect(conString,
(err, client) => {
client.query({text:"INSERT
INTO movies(title, year) VALUES($1, $2)", values: ['The Matrix','1999']}},
(err, res) => {
// check err to see if everything is ok.
})
})
对于删除和更新,语法完全相同。
这些是 Postgres 驱动程序的一些用途;在驱动程序的 GitHub 页面上有更高级的用例。
在 Postgres 的数据库中,我们需要的不仅仅是一个精简的驱动程序,还有很多其他的选项可以作为这个驱动程序的插件或者替代。
一种不同的方法是使用真实的对象关系映射器(ORM)将我们自己的对象映射到表中。
Node 中使用最多的 ORM 是 Sequelize 。Sequelize 不仅仅是给 Postgres 的;它也支持不同的数据库,如 MySQL、MariaDB 和 SQLite。
使用 Sequelize,我们可以定义一个存储在表中的对象:
代码清单 62
var Sequelize = require('sequelize');
var sequelize = new Sequelize('databaseName',
usr, 'pwd');
var Movie = sequelize.define('movie', {
title:
Sequelize.STRING,
year:
Sequelize.INTEGER
})
Movie
.findAll({where: ['year = ?, 1982]})
.success(movies => {
// do something with movie
})
这几行代码创建了一个Movie
模式,并在数据库中查询 1982 年的所有电影。如您所见,查询是使用特定于域的语言(DSL)而不是 SQL 编写的。使用sequelize.define
在模式中定义映射(哪个表和哪个字段)。
Movie
对象也可用于将新电影存储到数据库:
代码清单 63
const Sequelize = require('sequelize')
const sequelize = new Sequelize('databaseName', usr,
'pwd')
const Movie = sequelize.define('movie', {
title: Sequelize.STRING,
year: Sequelize.INTEGER
})
const movie = Movie.build({title: 'The Matrix',year: 1999})
movie.save()
build
方法在内存中创建新的电影,可以使用Save
方法持久化。正如在findAll
的例子中,我们不需要指定存储对象属性的表和字段。由于映射,所有这些都是自动完成的。
ORM 简化了许多基本的操作,但是就像我们项目中的每一个依赖一样,它产生了一种摩擦。Sequelize 支持很多特性,例如允许从一个对象开始加载相关表的关系,但是在某些方面,我们失去了对数据库访问的控制,这在大型应用中会导致性能问题。
MongoDB 是一种不同类型的数据库,被归类为 NoSQL 数据库,更像是一个文档数据库,因为您可以存储的最小粒度的信息是一个文档。
| | 注意:NoSQL 代表“不仅仅是 SQL”,它代表了一个使用不同于关系的机制来存储数据的数据库。有很多选择。MongoDB 将记录存储为 JSON 文档,但其他人使用不同的存储技术。维基百科很好地解释了 en.wikipedia.org/wiki/NoSQL 的各种数据库类型。 |
MongoDB 中的一个文档是 JSON(特别是 BSON,二进制 JSON),我们可以将其保存在一个集合(一种表)中。
和 Postgres 一样,有各种各样的包可以访问数据库,但是 MongoDB 团队已经发布了一个名为mongodb
的官方驱动程序。
开始使用 MongoDB 数据库的基本模板是:
代码清单 64
const client
= require('mongodb').MongoClient
const url =
'mongodb://localhost:27017/mydatabase'
client.connect(url,
(err, db) => {
// do
something here
db.close()
});
这段代码与我们编写的连接 Postgres 数据库的代码非常相似。我们使用连接字符串来标识数据库服务器,并调用connect
函数进行连接。
查询集合以读取文档也非常相似:
代码清单 65
const client = require('mongodb').MongoClient
const url =
'mongodb://localhost:27017/mydatabase'
client.connect(url, function(err,
db) {
const collection =
db.collection('movies')
collection.find({year: 1982}).toArray((err, movies) => {
// do something with movies array
})
db.close();
});
即使这段代码与 Postgres 代码非常相似,这里我们还有一个额外的步骤来将结果转换为 JavaScript 数组。默认情况下,mongodb
驱动程序返回一个光标,我们可以使用它来循环遍历元素,或者像前面的例子一样,将光标转换成标准数组来使用数据。
MongoDB 用来提取数据的查询语言是一种示例查询。在示例中,我们要求提取所有属性year
等于 1982 的文档。
要提取所有文档,没有任何子句,我们可以传递给find
一个空对象:
代码清单 66
// ...
collection.find({}).toArray((err,
movies) => {
// do something with movies array
})
为了更好地掌握 MongoDB 查询语法的特性,官方文档是最好的资源。
其他 CRUD 操作非常简单。请记住,在 MongoDB 中,您总是对文档进行操作,因此我们可以插入文档、修改文档和删除文档。
代码清单 67
const client = require('mongodb').MongoClient
const movie = {title: 'The Matrix',
year: 1999}
const url = 'mongodb://localhost:27017/mydatabase'
client.connect(url, function(err,
db) {
const collection =
db.collection('movies')
collection.insertOne(movie, (err, r) => {
// do something with movies array
})
db.close();
});
将 MongoDB 与 Node 一起使用的好处是它们使用相同的语言。我们可以透明地将普通的 JavaScript 对象传递给驱动程序,它将在不需要转换它们的情况下保存它们。MongoDB 使用 JSON 作为存储格式,Node 理解 JSON!
insertOne
命令将对象保存在 MongoDB 中。在回调内部,对象r
是一个insertOneWriteOpResult
,一种insert
操作的结果。它包含两个有趣的属性:insertedCount
,应该等于 1,因为我们只插入了一个文档,以及insertedId
,MongoDB 分配给我们文档的标识符。
如果我们没有指定_id
属性,MongoDB 会分配一个类型为ObjectId
的值(一种由 MongoDB 生成的 UUID)。这是文档的id
,我们可以用它来查询文档。
mongodb
驱动级别很低,API 有时候使用起来也很繁琐。与 MongoDB 一起工作的最有趣的包之一是 MongoDB 的对象数据映射器(ODM)。
Mongoose.js 提供了一些使用 MongoDB 的工具。它允许我们为集合定义一个模式,并以活动记录的方式处理对象,提供验证、管道拦截和查询语言。
Mongoose.js 需要几样东西才能工作:一个模式和一个模型实例。为了创建一个模式,我们必须使用 Mongoose 语法定义一个对象。例如,对于我们的电影数据库:
代码清单 68
// movieSchema.js
const mongoose = require('mongoose')
const movie = mongoose.Schema({
title: String,
year: Number
})
module.exports = mongoose.model('Movie', movie)
使用函数Schema
,我们为我们的集合定义了一个模式,在本例中是两个具有自己类型的属性。
| | 注意:即使 MongoDB 是一个无模式数据库,在集合中存储具有不同模式的对象也是非常罕见的。在这种情况下,您可以使用一个名为 mongose-schema-extend 的插件。 |
在模块的最后,我们基于该模式创建了一个模型。现在我们可以使用这个模型来查询数据库:
代码清单 69
const
mongoose = require('mongoose')
const Movie = require('./movieSchema')
Movie.find({year: 1989}, (err, movies) => {
// do something with movies array
})
通过要求Movie
模型,我们可以用它来查询数据库。查询语法与前面的默认驱动程序类似,但是在这种情况下,我们不必将光标转换为数组,因为我们从 Mongoose 获得的已经是一个文档数组。
猫鼬的真正力量在其他操作中。例如,创建一部新电影并将其保存到数据库非常容易:
代码清单 70
const
mongoose = require('mongoose')
const Movie = require('./movieSchema')
const movie = new Movie({title:'The Matrix', year: 1999})
movie.save()
在这个例子中,我们创建了一个电影的新实例,指定了标题和年份。有了 Mongoose 模型,我们可以只调用save
将其持久化到数据库中。save
函数接收到一个带有错误的可选回调,所以前面的例子是一个不检查错误的简化版本。
猫鼬管理着许多超出本书范围的其他事情。这个项目的网站有很好的文档记录,有很多有用的例子可以效仿。
所以用 Node.js 持久化数据和其他平台没什么区别。MongoDB 是摩擦较小的数据库,因为它处理的是 JSON 数据,但并不是所有的应用都需要文档数据库,有时好的旧 SQL 数据库是更安全的选择。