Express 数据库
Express 应用可以使用 Node 支持的所有数据库(Express 本身不支持数据库管理的任何具体行为/需求)。有许多流行的选择,包括 PostgreSQL、MySQL、Redis、SQLite 和 MongoDB。
与数据库交互有两种方法:
-
使用数据库的原生查询语言(例如 SQL)
-
使用对象数据模型(Object Data Model,简称 ODM)或对象关系模型(Object Relational Model,简称 ORM)。ODM / ORM 能将网站中的数据表示为 JavaScript 对象,然后将它们映射到底层数据库。一些 ORM 只适用某些特定数据库,还有一些是普遍适用的。
使用 SQL 或其他受到支持的查询语言才能达到最佳性能。ODM 通常慢一些,因为在对象和数据库格式之间存在一层用于映射的翻译代码,使它不一定会选用最高性能的数据库查询(尤其是普遍使用级别的 ODM,它必须在各类数据库功能方面做出更大的折衷)。
使用 ORM 的好处是:程序员可以继续用 JavaScript 对象的思维而不用转向数据库语义的思维。在(同一个或不同网站)使用不同数据库时尤为明显。使用 ORM 还可以更方便地对数据进行验证和检查。
NPM 站点上有许多 ODM / ORM 解决方案(另请参阅 NPM 站点上的 odm 和 orm 标签列表)。
以下是几种流行的解决方案:
-
Mongoose:一款为异步工作环境设计的 MongoDB 对象建模工具。
-
Waterline:从基于 Express 的 Sails 框架中提取的 ORM。它提供了一套统一的 API 来访问众多不同的数据库,其中包括 Redis,mySQL,LDAP,MongoDB 和 Postgres。
-
Bookshelf:同时提供基于 promise 和传统回调两套接口,支持事务处理、渴求式/嵌套渴求式关系加载、多态关联,以及对一对一,一对多和多对多关系。支持 PostgreSQL、MySQL 和 SQLite3。
-
Objection:以尽可能简单的方式使用 SQL 和底层数据库引擎的全部功能(支持 SQLite3、Postgres 和 MySQL)。
-
Sequelize:基于 promise 的 Node.js 版 ORM,它支持 PostgreSQL、MySQL、MariaDB、SQLite 和 MSSQL,并提供可靠的事务支持、关系、复本读取等功能。
-
Node ORM2:一款 Node.js 对象关系管理系统。支持 MySQL、SQLite 以及 Progress,可以帮助你用面向对象的方法操作数据库。
Mongoose 是最受欢迎的 ODM,选用 MongoDB 数据库时,它是一个合理的选择。
使用 Mongoose
Mongoose是一个MongoDB对象建模工具,设计用于异步环境。Mongoose支持Node.js和Deno(alpha)。
官方文档网站是 mongosejs.com。
安装 Mongoose 和 MongoDB
Mongoose 像任何其他依赖项一样,使用 NPM 将其安装在您的项目(package.json)中。请在项目文件夹中运行下面的命令以完成安装:
npm install mongoose
npm install mongoose
added 24 packages, and audited 150 packages in 20s
10 packages are looking for funding
run `npm fund` for details
7 vulnerabilities (2 low, 5 high)
To address issues that do not require attention, run:
npm audit fix
To address all issues, run:
npm audit fix --force
Run `npm audit` for details.
安装 Mongoose 会添加所有依赖项,包括 MongoDB 数据库驱动程序,但不会安装 MongoDB 本身。要安装 MongoDB 服务器,可以 点击下载 各操作系统的安装程序在本地安装。也可以使用云端 MongoDB 实例。
连接到 MongoDB
连接到 MongoDB
Mongoose 需要连接到 MongoDB 数据库。可以 require() 之,并通过 mongoose.connect() 连接到本地数据库,如下。
// 导入 mongoose 模块
const mongoose = require('mongoose');
// 设置默认 mongoose 连接
const mongoDB = 'mongodb://127.0.0.1/my_database';
mongoose.connect(mongoDB);
// 让 mongoose 使用全局 Promise 库
mongoose.Promise = global.Promise;
// 取得默认连接
const db = mongoose.connection;
// 将连接与错误事件绑定(以获得连接错误的提示)
db.on('error', console.error.bind(console, 'MongoDB 连接错误:'));
可以用 mongoose.connection 取得默认的 Connection 对象。一旦连接,Connection 实例将触发打开事件。
可以使用 mongoose.createConnection() 创建其他连接。该函数与 connect() 的参数(数据库 URI,包括主机地址、数据库名、端口、选项等)一致,并返回一个 Connection 对象。
定义和添加模型
模型使用 Schema 接口进行定义。 Schema 可以定义每个文档中存储的字段,及字段的验证要求和默认值。还可以通过定义静态和实例辅助方法来更轻松地处理各种类型的数据,还可以像使用普通字段一样使用数据库中并不存在的虚拟属性。
mongoose.model() 方法将模式“编译”为模型。模型就可以用来查找、创建、更新和删除特定类型的对象。
MongoDB 数据库中,每个模型都映射至一组文档。这些文档包含 Schema 模型定义的字段名/模式类型。
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
name: { type: String },
avatar: { type: String },
banner: { type: String },
title: { type: String },
categories: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Category' }],
scores: {
difficult: { type: Number },
skills: { type: Number },
attack: { type: Number },
survive: { type: Number },
},
skills: [{
icon: { type: String },
name: { type: String },
delay: { type: String },
cost: { type: String },
description: { type: String },
tips: { type: String },
}],
items1: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Item' }],
items2: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Item' }],
usageTips: { type: String },
battleTips: { type: String },
teamTips: { type: String },
partners: [{
hero: { type: mongoose.SchemaTypes.ObjectId, ref: 'Hero' },
description: { type: String },
}],
})
module.exports = mongoose.model('Hero', schema, 'heroes')
定义模式
下面的代码片段中定义了一个简单的模式。首先 require() mongoose,然后使用 Schema 构造器创建一个新的模式实例,使用构造器的对象参数定义各个字段。
// 获取 Mongoose
const mongoose = require('mongoose');
// 定义一个模式
var Schema = mongoose.Schema;
var SomeModelSchema = new Schema({
a_string: String,
a_date: Date
});
上面示例只有两个字段(一个字符串和一个日期),接下来将展示其他字段类型、验证和其他方法。
创建模型
使用 mongoose.model() 方法从模式创建模型:
// 定义模式
const Schema = mongoose.Schema;
const SomeModelSchema = new Schema({
a_string: String,
a_date: Date
});
// 使用模式“编译”模型
const SomeModel = mongoose.model('SomeModel', SomeModelSchema);
第一个参数是为模型所创建集合的别名(Mongoose 将为 SomeModel 模型创建数据库集合),第二个参数是创建模型时使用的模式。
定义模型类后,可以使用它们来创建、更新或删除记录,以及通过查询来获取所有记录或特定子集。我们将在以下“使用模型”部分展示,包括创建视图的情况。
模式类型(字段)
模式可以包含任意数量的字段,每个字段代表 MongoDB 文档中的一段存储区域。下面是一个模式的示例,其中有许多常见字段类型和声明方式:
const schema = new Schema(
{
name: String,
binary: Buffer,
living: Boolean,
updated: { type: Date, default: Date.now },
age: { type: Number, min: 18, max: 65, required: true },
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
array: [],
ofString: [String], // 其他类型也可使用数组
nested: { stuff: { type: String, lowercase: true, trim: true } }
})
-
ObjectId:表示数据库中某一模型的特定实例。例如,一本书可能会使用它来表示其作者对象。它实际只包含指定对象的唯一 ID(_id) 。可以使用 populate() 方法在需要时提取相关信息。
-
[]:对象数组。以在此类模型上执行 JavaScript 数组操作(push、pop、unshift等)。上例中有一个没有指定类型的对象数组和一个 String 对象数组,数组中的对象可以是任意类型的。
代码还展示了声明字段的两种方法:
-
字段名和类型名作为键 - 值对(就像 name、binary 和 living)。
-
字段名后跟一个对象,在对象中定义 type 和字段的其他选项,可以是:
默认值。
内置验证器(例如最大/最小值)和自定义验证函数。
该字段是否必需。
是否将 String 字段自动转换为小写、大写,或截断两端空格
例如 { type: String, lowercase: true, trim: true }
验证
Mongoose 提供内置的和自定义的验证器,以及同步的和异步的验证器。你可以在所有情况下,指定可接受的范围或值,以及验证失败的错误消息。
内置的验证器包括:
- 所有 模式类型 都具有内置的 required 验证器。用于指定当前字段是否为保存文档所必需的。
- Number 有数值范围验证器 min 和 max。
- String 有:
enum:指定当前字段允许值的集合。
match:指定字符串必须匹配的正则表达式。
字符串的最大长度 maxlength 和最小长度 minlength
以下是类型验证器和错误消息的设定方法
const breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, '鸡蛋太少'],
max: 12
},
drink: {
type: String,
enum: ['咖啡', '茶']
}
});
虚拟属性
虚拟属性是可以获取和设置、但不会保存到 MongoDB 的文档属性。getter 可用于格式化或组合字段,而 setter 可用于将单个值分解为多个值从而便于存储。文档中的示例,从名字和姓氏字段构造(并解构)一个全名虚拟属性,这比每次在模板中使用全名更简单,更清晰。
我们将使用库中的一个虚拟属性,用路径和记录的 _id 来为每个模型记录定义唯一的 URL。
schema.virtual('children', {
localField: '_id',
foreignField: 'parent',
justOne: false,
ref: 'Category'
})
方法和查询助手
模式支持 实例方法、静态方法 和 查询助手。实例方法和静态方法外表很相似,但有本质区别,实例方法针对特定记录,且可以访问当前对象。查询助手可用于扩展 Mongoose 的 链式查询 API(例如,在 find()、findOne() 和 findById() 方法外还可以添加一个“byName”查询)。
创建和修改文档
可以通过定义模型的实例并调用 save() 来创建记录。以下示例假定 SomeModel 是用现有模式创建的模型(只有一个字段 “name” ):
// 创建一个 SomeModel 模型的实例
const awesome_instance = new SomeModel({ name: '牛人' });
// 传递回调以保存这个新建的模型实例
awesome_instance.save( function (err) {
if (err) {
return handleError(err);
} // 已保存
});
记录的创建(以及更新、删除和查询)操作是异步的,可以提供一个回调函数在操作完成时调用。由于 API 遵循错误参数优先的惯例,因此回调的第一个参数必为错误值(或 null)。如果 API 需要返回一些结果,则将结果作为第二个参数。
还可以使用 create(),在定义模型实例的同时将其保存。回调的第一个参数返回错误,第二个参数返回新建的模型实例。
SomeModel.create(
{ name: '也是牛人' },
function(err, awesome_instance) {
if (err) {
return handleError(err);
} // 已保存
}
);
每个模型都有一个相关的连接(使用 mongoose.model() 时将做为默认连接)。可以通过创建新连接并对其调用 .model(),从而在另一个数据库上创建文档。
可以使用“圆点”加字段名来访问、修改新记录中的字段。操作后必须调用 save() 或 update() 以将改动保存回数据库。
// 使用圆点来访问模型的字段值
console.log(awesome_instance.name); // 控制台将显示 '也是牛人'
// 修改字段内容并调用 save() 以修改记录
awesome_instance.name = "酷毙了的牛人";
awesome_instance.save( function(err) {
if (err) {
return handleError(err);
} // 已保存
});
搜索记录
可以使用查询方法搜索记录,查询条件可列在 JSON 文档中。以下代码展示了如何在数据库中找到所有网球运动员,并返回运动员姓名和年龄字段。这里只指定了一个匹配字段(运动项目,sport),也可以添加更多条件,指定正则表达式,或去除所有条件以返回所有运动员。
const Athlete = mongoose.model('Athlete', yourSchema);
// SELECT name, age FROM Athlete WHERE sport='Tennis'
Athlete.find(
{ 'sport': 'Tennis' },
'name age',
function (err, athletes) {
if (err) {
return handleError(err);
} // 'athletes' 中保存一个符合条件的运动员的列表
}
);
若像上述代码那样指定回调,则查询将立即执行。搜索完成后将调用回调。
Mongoose 中所有回调都使用 callback(error, result) 模式。如果查询时发生错误,则参数 error 将包含错误文档,result 为 null。如果查询成功,则 error为 null,查询结果将填充至 result 。
若未指定回调,则 API 将返回 Query 类型的变量。可以使用该查询对象来构建查询,随后使用 exec() 方法执行(使用回调)。
http://mongoosejs.com/docs/api.html#query-js
// 寻找所有网球运动员
const query = Athlete.find({ 'sport': 'Tennis' });
// 查找 name, age 两个字段
query.select('name age');
// 只查找前 5 条记录
query.limit(5);
// 按年龄排序
query.sort({ age: -1 });
// 以后某个时间运行该查询
query.exec(function (err, athletes) {
if (err) {
return handleError(err);
} // athletes 中保存网球运动员列表,按年龄排序,共 5 条记录
})
上面的查询条件定义在 find() 方法中。也可以使用 where() 函数来执行此操作,可以使用点运算符(.)将所有查询链接在一起。以下代码与上述的查询基本相同,还添加了年龄范围的附加条件。
Athlete.
find().
where('sport').equals('Tennis').
where('age').gt(17).lt(50). // 附加 WHERE 查询
limit(5).
sort({ age: -1 }).
select('name age').
exec(callback); // 回调函数的名字是 callback
find() 方法会取得所有匹配记录,但通常你只想取得一个。以下方法可以查询单个记录:
findById():用指定 id 查找文档(每个文档都有一个唯一 id)。
findOne():查找与指定条件匹配的第一个文档。
findByIdAndRemove()、findByIdAndUpdate()、findOneAndRemove()、 findOneAndUpdate():通过 id 或条件查找单个文档,并进行更新或删除。以上是更新和删除记录的便利函数。
文档间协同 —— population
可以使用 ObjectId 模式字段来创建两个文档/模型实例间一对一的引用,(一组 ObjectIds 可创建一对多的引用)。该字段存储相关模型的 id。如果需要相关文档的实际内容,可以在查询中使用 populate() 方法,将 id 替换为实际数据。
例如,以下模式定义了作者和简介。每个作者可以有多条简介,我们将其表示为一个 ObjectId 数组。每条简介只对应一个作者。“ref” 告知模式分配哪个模型给该字段。
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const authorSchema = Schema({
name : String,
stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author : { type: Schema.Types.ObjectId, ref: 'Author' },
title : String
});
const Story = mongoose.model('Story', storySchema);
const Author = mongoose.model('Author', authorSchema);
可以通过分配 _id 值来保存对相关文档的引用。下面我们创建一个作者、一条简介,并将新简介的 author 字段设置为新建作者的 id。
const wxm = new Author({ name: '司马迁' });
wxm.save(function (err) {
if (err) {
return handleError(err);
}
// 现在库中有了作者司马迁,我们来新建一条简介
const story = new Story({
title: "司马迁是历史学家",
author: wxm._id // author 设置为作者 司马迁 的 _id。ID 是自动创建的。
});
story.save(function (err) {
if (err) {
return handleError(err);
} // 司马迁有了一条简介
});
});
现在简介文档通过作者文档的 ID 引用了作者。可使用 populate() 在简介中获取作者信息,如下所示。
Story
.findOne({ title: '司马迁是历史学家' })
.populate('author') // 使用作者 id 填充实际作者信息
.exec(function (err, story) {
if (err) {
return handleError(err);
}
console.log('作者是 %s', story.author.name);
// 控制台将打印 "作者是 司马迁"
});
一模式(模型)一文件
虽然创建模式和模型没有文件结构的限制,但强烈建议将单一模式定义在单一模块(文件)中,并通过导出方法来创建模型。
// 文件:./models/somemodel.js
// Require Mongoose
const mongoose = require('mongoose');
// 定义一个模式
const Schema = mongoose.Schema;
const SomeModelSchema = new Schema({
a_string : String,
a_date : Date
});
// 导出函数来创建 "SomeModel" 模型类
module.exports = mongoose.model('SomeModel', SomeModelSchema );
然后就可以在其他文件中,require 并使用该模型。下面是通过 SomeModel 模块来获取所有实例的方法。
// 通过 require 模块来创建 SomeModel 模型
const SomeModel = require('../models/somemodel')
// 使用 SomeModel 对象(模型)来查找所有的 SomeModel 记录
SomeModel.find(callback_function);