mongoose Api CRUD操作返回结果总结梳理

大体概念

mongoose 中的Model是 由 Schema 编译来的构造函数,类似于JavaScript中的类,负责从底层MongoDB数据库创建和读取文档(CRUD操作);Document就是Model的一个实例,SchemaModel定型,指定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的理解,如果只是作为前端搭建简单的服务器了解即可

中间件 (也叫 prepost 钩子) 是在异步函数执行时传入的控制函数。Mongoose 有四种中间件: document 中间件,model 中间件,aggregate 中间件,和 query 中间件。

所有的中间件都支持 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)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容