MongoDB 的 Mongoose 简介
Mongoose 是 MongoDB 和 Node.js 的对象数据建模 (ODM) 库。它管理数据之间的关系,提供模式验证,并用于在代码中的对象与 MongoDB 中的对象表示之间进行转换。
MongoDB 是一个无模式的 NoSQL 文档数据库。这意味着您可以在其中存储 JSON 文档,并且这些文档的结构可以变化,因为它不像 SQL 数据库那样强制执行。这是使用 NoSQL 的优势之一,因为它可以加快应用程序开发速度并降低部署的复杂性。
以下是 Mongo 与 SQL 数据库中数据存储方式的示例:

术语
收藏
Mongo 中的“集合”相当于关系数据库中的表。它们可以保存多个 JSON 文档。
文件
“文档”相当于 SQL 中的记录或数据行。虽然 SQL 行可以引用其他表中的数据,但 Mongo 文档通常会将这些数据合并到一个文档中。
字段
“字段”或属性类似于 SQL 表中的列。
架构
Mongo 不依赖模式,而 SQL 则通过表定义来定义模式。Mongoose 的“模式”是一种文档数据结构(或文档的形状),由应用层强制执行。
模型
“模型”是高阶构造函数,它采用模式并创建相当于关系数据库中的记录的文档实例。
入门
Mongo 安装
在开始之前,我们先来设置一下 Mongo。你可以从以下选项中选择一种(本文使用选项 1):
- 从MongoDB 网站下载适合您的操作系统的 MongoDB 版本,并按照其安装说明进行操作
- 在 mLab 上创建免费的沙盒数据库订阅
- 如果您喜欢使用Docker,请使用 Docker 安装 Mongo
让我们通过实现一个表示简化地址簿数据的模型来了解 Mongoose 的一些基础知识。
我使用的是 Visual Studio Code、Node 8.9 和 NPM 5.6。启动你最喜欢的 IDE,创建一个空白项目,然后开始吧!我们将使用 Node 中有限的 ES6 语法,因此无需配置 Babel。
NPM安装
让我们进入项目文件夹并初始化我们的项目
npm init -y
让我们使用以下命令安装 Mongoose 和验证库:
npm install mongoose validator
上述安装命令将安装最新版本的库。本文中的 Mongoose 语法仅适用于 Mongoose v5 及更高版本。
数据库连接
在项目根目录下创建一个文件./src/database.js 。
接下来,我们将添加一个带有连接数据库的方法的简单类。
您的连接字符串将根据您的安装而有所不同。
let mongoose = require('mongoose');
const server = '127.0.0.1:27017'; // REPLACE WITH YOUR DB SERVER
const database = 'fcc-Mail'; // REPLACE WITH YOUR DB NAME
class Database {
constructor() {
this._connect()
}
_connect() {
mongoose.connect(`mongodb://${server}/${database}`)
.then(() => {
console.log('Database connection successful')
})
.catch(err => {
console.error('Database connection error')
})
}
}
module.exports = new Database()
上面的 require('mongoose') 调用返回一个单例对象。这意味着第一次调用 require('mongoose') 时,它会创建并返回 Mongoose 类的一个实例。由于 ES6 中模块导入/导出的机制,后续调用时它将返回与第一次创建并返回的实例相同的实例。
类似地,我们通过在 module.exports 语句中返回类的实例将我们的数据库类变成了单例,因为我们只需要一个数据库连接。
由于模块加载器通过缓存先前导入的文件的响应来工作,ES6 使我们能够非常轻松地创建单例(单实例)模式。
Mongoose Schema 与 Model
Mongoose 模型是 Mongoose 模式的包装器。Mongoose 模式定义了文档的结构、默认值、验证器等,而 Mongoose 模型提供了数据库接口,用于创建、查询、更新、删除记录等。
创建 Mongoose 模型主要包括三个部分:
1. 引用 Mongoose
let mongoose = require('mongoose')
此引用与我们连接到数据库时返回的引用相同,这意味着模式和模型定义不需要明确连接到数据库。
2. 定义模式
模式通过对象定义文档属性,其中键名对应于集合中的属性名。
let emailSchema = new mongoose.Schema({
email: String
})
这里我们定义了一个名为email的属性,其模式类型为String,它映射到一个内部验证器,当模型保存到数据库时会触发该验证器。如果值的数据类型不是字符串类型,验证将会失败。
允许使用以下架构类型:
- 大批
- 布尔值
- 缓冲
- 日期
- 混合(通用/灵活的数据类型)
- 数字
- 对象ID
- 细绳
Mixed 和 ObjectId 在 require('mongoose').Schema.Types 下定义。
3.导出模型
我们需要在 Mongoose 实例上调用模型构造函数,并将集合的名称和对模式定义的引用传递给它。
module.exports = mongoose.model('Email', emailSchema)
我们将上面的代码合并到./src/models/email.js中来定义一个基本的电子邮件模型的内容:
let mongoose = require('mongoose')
let emailSchema = new mongoose.Schema({
email: String
})
module.exports = mongoose.model('Email', emailSchema)
模式定义应该简单,但其复杂性通常取决于应用需求。模式可以重用,并且可以包含多个子模式。在上面的示例中,email 属性的值是一个简单的值类型。然而,它也可以是带有附加属性的对象类型。
我们可以创建上面定义的模型的实例,并使用以下语法填充它:
let EmailModel = require('./email')
let msg = new EmailModel({
email: 'ada.lovelace@gmail.com'
})
让我们增强 Email 架构,使 email 属性成为唯一的必填字段,并在保存之前将其值转换为小写。我们还可以添加一个验证函数,以确保该值是有效的电子邮件地址。我们将引用并使用之前安装的验证器库。
let mongoose = require('mongoose')
let validator = require('validator')
let emailSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
validate: (value) => {
return validator.isEmail(value)
}
}
})
module.exports = mongoose.model('Email', emailSchema)
基本操作
Mongoose 拥有灵活的 API,并提供多种方法来完成任务。由于这些方法超出了本文的讨论范围,因此我们不会重点介绍其变体。但请记住,大多数操作可以通过多种方式实现,无论是语法上还是通过应用程序架构。
创建记录
让我们创建电子邮件模型的实例并将其保存到数据库中:
let EmailModel = require('./email')
let msg = new EmailModel({
email: 'ADA.LOVELACE@GMAIL.COM'
})
msg.save()
.then(doc => {
console.log(doc)
})
.catch(err => {
console.error(err)
})
结果是成功保存后返回的文档:
{
_id: 5a78fe3e2f44ba8f85a2409a,
email: 'ada.lovelace@gmail.com',
__v: 0
}
返回以下字段(内部字段以下划线为前缀):
- _id 字段由 Mongo 自动生成,是集合的主键。其值是文档的唯一标识符。
- 返回 email 字段的值。注意,它是小写的,因为我们在 schema 中指定了 lowercase:true 属性。
- __v 是 Mongoose 首次创建每个文档时设置的 versionKey 属性。其值包含文档的内部修订版本。
如果您尝试重复上述保存操作,您将收到错误,因为我们已经指定电子邮件字段应该是唯一的。
获取记录
让我们尝试检索之前保存到数据库的记录。模型类公开了几个静态方法和实例方法来对数据库执行操作。现在,我们将尝试使用 find 方法查找之前创建的记录,并将电子邮件作为搜索词传递。
EmailModel
.find({
email: 'ada.lovelace@gmail.com' // search query
})
.then(doc => {
console.log(doc)
})
.catch(err => {
console.error(err)
})
返回的文档将与我们创建记录时显示的文档类似:
{
_id: 5a78fe3e2f44ba8f85a2409a,
email: 'ada.lovelace@gmail.com',
__v: 0
}
更新记录
让我们通过一次操作来修改上面的记录,修改电子邮件地址并添加另一个字段。出于性能考虑,Mongoose 不会返回更新后的文档,因此我们需要传递一个额外的参数来获取它:
EmailModel
.findOneAndUpdate(
{
email: 'ada.lovelace@gmail.com' // search query
},
{
email: 'theoutlander@live.com' // field:values to update
},
{
new: true, // return updated doc
runValidators: true // validate before update
})
.then(doc => {
console.log(doc)
})
.catch(err => {
console.error(err)
})
返回的文档将包含更新后的电子邮件:
{
_id: 5a78fe3e2f44ba8f85a2409a,
email: 'theoutlander@live.com',
__v: 0
}
删除记录
我们将使用 findOneAndRemove 调用来删除一条记录。它返回被删除的原始文档:
EmailModel
.findOneAndRemove({
email: 'theoutlander@live.com'
})
.then(response => {
console.log(response)
})
.catch(err => {
console.error(err)
})
助手
上面我们已经了解了一些称为 CRUD(创建、读取、更新、删除)操作的基本功能,但 Mongoose 还提供了配置多种辅助方法和属性的功能。这些可以用来进一步简化数据处理。
让我们在./src/models/user.js中创建一个包含字段 firstName 和 lastName 的用户模式:
let mongoose = require('mongoose')
let userSchema = new mongoose.Schema({
firstName: String,
lastName: String
})
module.exports = mongoose.model('User', userSchema)
虚拟财产
虚拟属性不会持久化到数据库中。我们可以将其添加到架构中,作为获取和设置值的辅助方法。
让我们创建一个名为 fullName 的虚拟属性,它可用于设置 firstName 和 lastName 的值,并在读取时将它们作为组合值检索:
userSchema.virtual('fullName').get(function() {
return this.firstName + ' ' + this.lastName
})
userSchema.virtual('fullName').set(function(name) {
let str = name.split(' ')
this.firstName = str[0]
this.lastName = str[1]
})
get 和 set 的回调必须使用 function 关键字,因为我们需要通过 this 关键字访问模型。使用箭头函数会改变 this 的指向。
现在,我们可以通过为 fullName 分配一个值来设置 firstName 和 lastName:
let model = new UserModel()
model.fullName = 'Thomas Anderson'
console.log(model.toJSON()) // Output model fields as JSON
console.log()
console.log(model.fullName) // Output the full name
上面的代码将输出以下内容:
{ _id: 5a7a4248550ebb9fafd898cf,
firstName: 'Thomas',
lastName: 'Anderson' }
Thomas Anderson
实例方法
我们可以在架构上创建自定义辅助方法,并通过模型实例访问它们。这些方法将访问模型对象,并且可以灵活地使用它们。例如,我们可以创建一个方法来查找所有与当前实例同名的人员。
在此示例中,我们创建一个函数来返回当前用户的姓名首字母。我们向架构中添加一个名为 getInitials 的自定义辅助方法:
userSchema.methods.getInitials = function() {
return this.firstName[0] + this.lastName[0]
}
该方法可以通过模型实例访问:
let model = new UserModel({
firstName: 'Thomas',
lastName: 'Anderson'
})
let initials = model.getInitials()
console.log(initials) // This will output: TA
静态方法
与实例方法类似,我们可以在模式上创建静态方法。让我们创建一个方法来检索数据库中的所有用户:
userSchema.statics.getUsers = function() {
return new Promise((resolve, reject) => {
this.find((err, docs) => {
if(err) {
console.error(err)
return reject(err)
}
resolve(docs)
})
})
}
在 Model 类上调用 getUsers 将返回数据库中的所有用户:
UserModel.getUsers()
.then(docs => {
console.log(docs)
})
.catch(err => {
console.error(err)
})
添加实例和静态方法是实现集合和记录上的数据库交互接口的好方法。
中间件
中间件是在管道特定阶段运行的函数。Mongoose 支持以下操作的中间件:
- 总计的
- 文档
- 模型
- 询问
例如,模型具有前置函数和后置函数,它们接受两个参数:
- 事件类型(‘初始化’、‘验证’、‘保存’、‘删除’)
- 引用模型实例执行的回调
让我们尝试一个例子,在我们的模式中添加两个名为 createdAt 和 updatedAt 的字段:
let mongoose = require('mongoose')
let userSchema = new mongoose.Schema({
firstName: String,
lastName: String,
createdAt: Date,
updatedAt: Date
})
module.exports = mongoose.model('User', userSchema)
当调用 model.save() 时,会触发 pre('save', …) 和 post('save', …) 事件。对于第二个参数,您可以传递一个函数,该函数在触发事件时会被调用。这些函数会将参数传递给中间件链中的下一个函数。
让我们添加一个预保存钩子并设置 createdAt 和 updatedAt 的值:
userSchema.pre('save', function (next) {
let now = Date.now()
this.updatedAt = now
// Set a value for createdAt only if it is null
if (!this.createdAt) {
this.createdAt = now
}
// Call the next function in the pre-save chain
next()
})
让我们创建并保存我们的模型:
let UserModel = require('./user')
let model = new UserModel({
fullName: 'Thomas Anderson'
}
msg.save()
.then(doc => {
console.log(doc)
})
.catch(err => {
console.error(err)
})
当打印创建的记录时,您应该看到 createdAt 和 updatedAt 的值:
{ _id: 5a7bbbeebc3b49cb919da675,
firstName: 'Thomas',
lastName: 'Anderson',
updatedAt: 2018-02-08T02:54:38.888Z,
createdAt: 2018-02-08T02:54:38.888Z,
__v: 0 }
插件
假设我们想要追踪数据库中每个集合中某条记录的创建和最后更新时间。与其重复上述过程,不如创建一个插件并将其应用于每个 schema。
让我们创建一个文件./src/model/plugins/timestamp.js并将上述功能复制为可重用模块:
module.exports = function timestamp(schema) {
// Add the two fields to the schema
schema.add({
createdAt: Date,
updatedAt: Date
})
// Create a pre-save hook
schema.pre('save', function (next) {
let now = Date.now()
this.updatedAt = now
// Set a value for createdAt only if it is null
if (!this.createdAt) {
this.createdAt = now
}
// Call the next function in the pre-save chain
next()
})
}
要使用这个插件,我们只需将其传递给应该赋予此功能的模式:
let timestampPlugin = require('./plugins/timestamp')
emailSchema.plugin(timestampPlugin)
userSchema.plugin(timestampPlugin)
查询构建
Mongoose 拥有非常丰富的 API,可以处理 MongoDB 支持的许多复杂操作。考虑一个查询,我们可以逐步构建查询组件。
在此示例中,我们将:
- 查找所有用户
- 跳过前 100 条记录
- 将结果限制为 10 条记录
- 按 firstName 字段对结果进行排序
- 选择名字
- 执行该查询
UserModel.find() // find all users
.skip(100) // skip the first 100 items
.limit(10) // limit to 10 items
.sort({firstName: 1} // sort ascending by firstName
.select({firstName: true} // select firstName only
.exec() // execute the query
.then(docs => {
console.log(docs)
})
.catch(err => {
console.error(err)
})
结束语
我们仅仅触及了 Mongoose 的一些功能。它是一个功能丰富的库,充满了实用且强大的功能,让在应用层使用数据模型变得非常愉快。
虽然您可以使用 Mongo Driver 直接与 Mongo 交互,但 Mongoose 允许您对数据之间的关系进行建模并轻松验证它们,从而简化该交互。
有趣的事实: Mongoose是由Valeri Karpov创建的,他是一位才华横溢的工程师!他创造了“MEAN Stack”这个术语。
单击此处查看我即将开设的课程:Mongoose 完整开发者指南
如果这篇文章有帮助,请❤️并在 Twitter 上关注我。
鏂囩珷鏉ユ簮锛�https://dev.to/theoutlander/introduction-to-mongoose-for-mongodb--3c30