Skip to content

Latest commit

 

History

History
354 lines (244 loc) · 13 KB

07.md

File metadata and controls

354 lines (244 loc) · 13 KB

七、访问数据库

每个应用迟早都需要存储来保存其数据。Node.js 有几个包可以与几乎每个数据库通信。在本章中,我们将看到使用 PostgreSQLMongoDB 的一些基本操作,这两个数据库在它们自己的类别中使用最多。

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对象有两个事件:rowend。从数据库中提取的每一个新行都会引发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 数据库是更安全的选择。