大体概念
mongoose 中的Model
是 由 Schema
编译来的构造函数,类似于JavaScript中的类,负责从底层MongoDB数据库创建和读取文档(CRUD操作);Document
就是Model
的一个实例,Schema
给Model
定型,指定Model
包含的字段/
值的类型/
是否必传等等规则;而Query
则是定义好的命令,Model
的一些辅助函数返回的就是一个Query
对象。放在MySQL相当于一条待执行的sql语句,告诉数据库这具体是一个什么操作。
增加(Create)、检索(Retrieve)、更新(Update)和删除(Delete)
我另一篇文 mongoose 5.x中的Promise和async/await
提到,每一个Mongoose Query,接收的回调都遵循 callback(error, result)
的模式。
传了回调即意味着执行操作。如果执行时发生错误,返回的error
即是错误文档, result 则会是 null
;若未出错,error 为 null
,result 即是查询的结果。如果我们不传回调,用 async/await
的 方式 去获取返回,那返回值的内容等效于 result, error 就要靠我们用 catch 去捕获了。
当成功返回数据(err 为 null
),result
的格式取决于做的是什么操作:
新增文档: Model.create() : ( Array|Object ) 成功创建的数据文档 / 文档数组
查询单条文档: Model.findOne()、Model.findById() :( result: Object ) 未查到符合条件的文档则是
null
查询文档列表: Model.find() :( result: Array ) 符合
filter
条件的文档数组,没有符合条件的则是空数组[]
查询文档数量: Model.countDocuments():( result: Number ) 没有符合条件的则是
0
更新单个文档/文档列表:
(1) Model.update()、Model.updateMany()、Model.updateOne():( result: Object )
Object 参数说明:
ok
: Number (err 为 null 必然是1)
n
: Number (匹配filter条件的数量)
nModified: Number
: 被更新的文档数量。(如果没查到符合条件的数据,则是0
,也就是更新了0 条,不等于操作失败)。
(2) Model.findOneAndUpdate()、Model.findByIdAndUpdate():( result: Object ) 更新之前的文档(默认),如果在options传了new: true
,则是成功更新之后的文档。没查到匹配的则是null
删除单个/多个文档:
(1) Model.deleteMany()、Model.deleteOne():( result: Object )
Object 参数说明:
ok
: Number (err 为 null 必然是1)
deletedCount: Number
: 被删除的文档数量。(如果没查到符合条件的数据,则是0
,也就是删除了0 条,不等于操作失败)
n: Number
: 已删除文档的数量。等于deletedCount。
(2) Model.findOneAndDelete()、Model.findByIdAndDelete():( result: Object ) 查询到(被删)的这条文档,如果没符合条件的则是null
Models API 文档中有详细描述被传给回调函数的值。
我们做增删改查操作都是通过Model方法,看过Middleware这一节就会明白,调用这些Api实际上是去触发一些更深层的方法,比如
Document.save()
、Query.prototype.find()
。Model.create()
、Model.find()
等等都是被包装过的快捷方式(语法糖)。(下面 中间件 和 Api返回值细则 部分会详细讲)
使用示例
同时我们也提到过因为Query返回的不是Promise,通过加上exec()
便能返回完整的Promise,所以我们现在一般结合await
获取 result(推荐):
try {
const doc = await MyModel.findOne({ country: 'Croatia' }).exec()
// 如果没有符合 { country: 'Croatia' } 的数据,返回 `null`
if(doc === null) {
return res.json({
code: 400,
message: '没有符合条件的数据'
})
}
res.json({
code: 200,
data: docs
message: '查询成功'
})
} catch (err) {
console.log(err)
}
也可使用回调拿 result:
// 查找 name 含有 john 的数据,并且返回的列表中的对象只需要 "name" 和 "friends" 这两个字段
MyModel.find({ name: /john/i }, 'name friends').exec((err, docs) => {
if(err) return next(err)
if(docs.length === 0) {
return res.json({
code: 400,
message: '没有符合条件的数据'
})
}
console.log(docs)
})
关于中间件(这一部分主要为了加深对mongoose操作数据Api的理解,如果只是作为前端搭建简单的服务器了解即可
)
中间件 (也叫 pre
和 post
钩子) 是在异步函数执行时传入的控制函数。Mongoose 有四种中间件: document 中间件,model 中间件,aggregate 中间件,和 query 中间件。
Document 中间件支持这些document方法:validate、save、remove、updateOne、deleteOne、init (note: init钩子 是同步 synchronous)
Query 中间件支持如下 Model 和 Query 的方法:count、deleteMany、deleteOne、find、findOne、findOneAndDelete、findOneAndRemove、findOneAndUpdate、remove、update、updateOne、updateMany
Aggregate 中间件用于' MyModel.aggregate() ':aggregate。当你在一个Aggregate对象上调用' exec() '时,Aggregate 中间件会执行。在 aggregate 中间件,
this
指向aggregation对象Model 中间件支持以下model方法:insertMany. 在 model 中间件函数中,
this
指向当前model。
所有的中间件都支持 pre 和 post 钩子,它们的工作方式是怎样的呢:
错误处理
如果 pre 钩子出错,mongoose 将不会执行后面的函数。 Mongoose 会向回调函数传入 err 参数, 或者 reject 返回的 promise。
schema.pre('save', function(next) {
const err = new Error('something went wrong');
// If you call `next()` with an argument, that argument is assumed to be
// an error.
next(err);
});
schema.pre('save', function() {
// You can also return a promise that rejects
return new Promise((resolve, reject) => {
reject(new Error('something went wrong'));
});
});
schema.pre('save', function() {
// You can also throw a synchronous error
throw new Error('something went wrong');
});
schema.pre('save', async function() {
await Promise.resolve();
// You can also throw an error in an `async` function
throw new Error('something went wrong');
});
// save做的更改不会执行到 MongoDB,因为一个pre钩子(预处理钩子)出错了
myDoc.save(function(err) {
console.log(err.message); // 出错了
});
Post 中间件
post 中间件在方法执行之后调用,这个时候每个 pre
中间件都已经完成。
异步 Post 钩子
如果你给回调函数传入两个参数,mongoose 会认为第二个参数是 next()
函数,你可以通过 next 触发下一个中间件
// Takes 2 parameters: this is an asynchronous post hook
schema.post('save', function(doc, next) {
setTimeout(function() {
console.log('post1');
// Kick off the second post hook
next();
}, 10);
});
// Will not execute until the first middleware calls `next()`
schema.post('save', function(doc, next) {
console.log('post2');
next();
});
Save/Validate Hooks
save()
方法会触发validate()
钩子,因为mongoose有一个内置的pre('save')
钩子会调用validate()
。这意味着所有的pre('validate')
和post('validate')
钩子都会在任何pre('save')
钩子之前被调用。
schema.pre('validate', function() {
console.log('this gets printed first');
});
schema.post('validate', function() {
console.log('this gets printed second');
});
schema.pre('save', function() {
console.log('this gets printed third');
});
schema.post('save', function() {
console.log('this gets printed fourth');
});
findAndUpdate() 与 Query 中间件注意事项
Pre 和 post save() 钩子都不会在update()
, findOneAndUpdate()
等 Query 方法上执行。
Query 中间件与 document 中间件有一个细微但重要的区别:在 document 中间件的this
指的是正在被更新的文档。而query 中间件,mongoose不一定有一个对正在更新的文档的引用,所以它的this
指向query对象。
利用这一点,如果你想为每个updateOne()调用添加一个updatedAt时间戳,可以使用下面的pre钩子。
schema.pre('updateOne', function() {
// this指 query 对象
this.set({ updatedAt: new Date() });
});
同样的,你不能在pre('updateOne')
或pre('findOneAndUpdate')
query中间件中访问要被更新的文档。如果需要访问这个文档,则需要对该文档执行显式查询。如下:
schema.pre('findOneAndUpdate', async function() {
const docToUpdate = await this.model.findOne(this.getQuery());
console.log(docToUpdate); // The document that `findOneAndUpdate()` will modify
});
Error Handling Middleware
中间件的执行通常在出现错误并调用next(err)时停止。然而有一种特殊的post中间件称为“错误处理中间件”,它特定在错误发生时执行。错误处理中间件对于报告错误和提高错误消息的可读性非常有用。
错误处理中间件比普通中间件多一个 error 参数,并且这个 error 作为第一个参数传入。 然后错误处理中间件可以让你自由地进行错误的后续处理。
const schema = new Schema({
name: {
type: String,
// Will trigger a MongoError with code 11000 when
// you save a duplicate
unique: true
}
});
// Handler **must** take 3 parameters: the error that occurred, the document
// in question, and the `next()` function
schema.post('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next();
}
});
// Will trigger the `post('save')` error handler
Person.create([{ name: 'Axl Rose' }, { name: 'Axl Rose' }]);
query 中间件也可以使用错误处理中间件。你可以定义一个 post update() 钩子, 它可以捕获 MongoDB 重复 key 错误。
// The same E11000 error can occur when you call `update()`
// This function **must** take 3 parameters. If you use the
// `passRawResult` function, this function **must** take 4
// parameters
schema.post('update', function(error, res, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(); // The `update()` call will still error out.
}
});
const people = [{ name: 'Axl Rose' }, { name: 'Slash' }];
Person.create(people, function(error) {
Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } }, function(error) {
// `error.message` will be "There was a duplicate key error"
});
});
错误处理中间件可以转换错误,但不能移除错误。只要没有调用next(error)
把错误传递给后面的中间件去处理,这个中间件的调用仍然会出错。
————————————————————————————————————————
*主要看这里*
result 参数具体格式细则表【即 (err, result) 中的 result】
新增数据
实际触发的中间件方法 Document.prototype.save():如果该document的
isNew
参数是true
,则会在数据库中插入一个新文档。否则,只会发送一个updateOne
操作对原数据进行修改。
Model.create()
docs : Array|Object
(返回成功创建的数据文档/文档数组)
触发的中间件: save()
create()
是向数据库保存一条或多条文档的快捷方式。MyModel.create(docs)
会对docs的每一条文档执行一次new MyModel(doc).save()
查询数据
Mongoose支持 MongoDB丰富的查询语法 。可以用
Model.
find, findById, findOne, or where 静态方法来检索文档。
(1) 查询符合条件的第一条数据
Query.prototype.findOne()
doc: Object
(没有符合条件的是 null
)
基于它的方法:Model.findOne()、Model.findById() (Model
的一些方法返回Query
对象,实质上调用的就是Query
的原型方法「作用是声明一个操作命令」,result
一致故而可以参考,下文不再重复说明 )
(2) 查询符合条件的全部文档
Query.prototype.find()
docs: Array
(没有符合条件的是 []
)
基于它的方法:Model.find()
(3) 查询符合条件的文档数量
Query.prototype.countDocuments()
count: Number
(没有符合条件的是 0
)
基于它的方法:Model.countDocuments()
注⚠️:count() 已被弃用,请用countDocuments()
替代
更新数据
更新数据有两种方式,先检索 findOne() 再保存 save() 通常是使用Mongoose更新文档的正确方式。通过
save()
,我们可以得到完整的验证和中间件。
但当我们仅仅需要更新而不需要获取数据时,一些Query方法就很适合我们,比如 updateMany()。
update()
, updateMany()
, findOneAndUpdate()
等不会执行save()
中间件,因此也不会在修改数据库时先执行任何钩子或验证。如果需要完全成熟的验证,可以使用先检索文档再save()
的传统方法。
(1) 更新单条数据
Query.prototype.findOneAndUpdate()
doc: Object
默认是更新之前的文档对象,如果在options传了new: true
,则返回成功更新之后的文档对象。没查到匹配的则是null
发起的 mongodb
命令:findAndModify 更新指令。
基于它的方法:Model.findOneAndUpdate()、Model.findByIdAndUpdate()
Query.prototype.updateOne
writeOpResult: Object
内含参数如下:
ok
: Number (err 为 null 必然是1)
n
: Number (匹配filter条件的数量)
nModified: Number
: 被更新的文档数量。(如果没查到符合条件的数据,则是 0
,也就是更新了0 条,不等于操作失败)
基于它的方法:Model.updateOne()
{ n: 1, nModified: 1, ok: 1 }
只更新匹配filter的第一个文档。
(2) 更新多条数据
Query.prototype.update
writeOpResult: Object
对象内容同 UpdateOne
基于它的方法:Model.update()
Query.prototype.updateMany
writeOpResult: Object
对象内容同 UpdateOne
基于它的方法:Model.updateMany()
删除数据
(1) 删除单条数据
Query.prototype.deleteOne()
mongooseDeleteResutl: Object
,内含三个参数:
ok
: Number (err 为 null 必然是1)
deletedCount: Number
: 被删除的文档数量。(如果没查到符合条件的数据,则是 0
,也就是删除了0 条,不等于操作失败)
n: Number
: 已删除文档的数量。等于deletedCount。
这个方法调用 MongoDB driver's Collection#deleteOne()
function.
基于它的方法:Model.deleteOne()
{ok: 1, deletedCount:1, n: 1}
Query.prototype.findOneAndDelete()
doc: Object
查询到的(要被删的)的这条数据,如果没查到符合的则是 null
需要返回被删文档数据的时候使用。
发起的 MongoDB
命令: findOneAndDelete 指令
基于它的方法:Model.findOneAndDelete()、Model.findByIdAndDelete()
(2) 删除多条数据
Query.prototype.deleteMany()
mongooseDeleteResult: Object
对象内容同 deleteOne
这个方法调用MongoDB driver的Collection#deleteMany()
function。
基于它的方法:Model.deleteMany()
示例:
const id = req.params.id.split(',')
try {
const response = await Model.deleteMany({ _id: { $in: id } }).exec()
if(response.ok && response.deletedCount) {
res.json({
code: 200,
message: '批量删除成功'
})
} else{
res.json({
code: 400,
message: '删除失败'
})
}
} catch (err) {
next(err)
}