资讯详情

资讯详情

Mongoose Promise 与 async/await 完全指南:查询、exec() 与 thenable 机制解析

Mongoose Promise 与 async/await 完全指南查询、exec() 与 thenable 机制解析【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseMongoose 作为 MongoDB 的对象建模层其异步操作如.save()、各类查询全部基于 Promise 设计。本文以 docs/promises.md 为主线深入讲解 Mongoose 内置 Promise 的返回类型、查询为何不是真正的 Promise、thenable 机制的原理以及await与.exec()的正确搭配方式并结合仓库源码如 lib/query.js、lib/model.js与测试用例帮助你彻底理解并写出稳定、可调试的异步代码。一、内置 Promise异步操作返回 thenableMongoose 的所有异步操作——无论是文档的.save()还是各类查询.find()、.findOne()、.updateOne()等——返回的都是thenable 对象。这意味着你可以直接写出这样的代码MyModel.findOne({}).then(doc { // 使用查询结果 }); // 如果你使用 async/await const doc await MyModel.findOne({}).exec();从源码角度验证在 lib/model.js 中Model.prototype.save被定义为async function save(options)因此.save()返回的是一个真正的原生 Promise。这一点在文档示例中得到印证const gnr new Band({ name: Guns N\ Roses, members: [Axl, Slash] }); const promise gnr.save(); assert.ok(promise instanceof Promise); // save() 返回真正的 Promise promise.then(function(doc) { assert.equal(doc.name, Guns N\ Roses); });也就是说文档保存类操作天然返回 Promise可以直接await或链式.then()。而查询类操作的情况则要复杂一些详见下文。具体某个 API 操作的返回类型可查阅项目内的 api 文档源码生成自 docs/source/api.js。二、查询不是 Promisethenable 与真正 Promise 的区别与.save()不同Mongoose 查询Query并不是 Promise。虽然它带有.then()方法可以配合co、async/await 使用但它只是方便起见提供的接口。如果需要完整的 Promise 语义必须使用.exec()。文档中的示例清楚地演示了这一区别const query Band.findOne({ name: Guns N\ Roses }); assert.ok(!(query instanceof Promise)); // 查询不是 Promise // 查询虽然不是完整 Promise但确实有 .then() query.then(function(doc) { // 使用 doc }); // .exec() 返回完整的 Promise const promise Band.findOne({ name: Guns N\ Roses }).exec(); assert.ok(promise instanceof Promise); promise.then(function(doc) { // 使用 doc });这一设计可以从源码结构得到印证。在 lib/query.js 中Query.prototype.then的实现是Query.prototype.then function(resolve, reject) { return this.exec().then(resolve, reject); };同理lib/query.js 的.catch()与 lib/query.js 的.finally()也都是对.exec()的薄包装Query.prototype.catch function(reject) { return this.exec().then(null, reject); }; Query.prototype.finally function(onFinally) { return this.exec().finally(onFinally); };从源码可以推断Query 的 thenable 支持本质上是委托给exec()返回的 PromiseQuery 对象自身并不具备 Promise 的完整内部状态机如reject/resolve状态管理这也是查询不是 Promise的底层原因。三、查询是 thenable链式与 await 均可使用尽管查询不是 Promise但查询是标准的thenable即拥有.then()方法的对象遵循 Promises/A 术语 定义。因此查询对象可以被 Promise 解析机制Promise.resolve()、promise 链以及 async/await 当作 Promise 来消费Band.findOne({ name: Guns N\ Roses }).then(function(doc) { // 使用 doc });// 也可以直接 await 查询对象本身 const doc await Band.findOne({ name: Guns N\ Roses });这里需要特别注意的是查询的一次性执行特性。在 lib/query.js 的exec()实现中通过_execCount计数器防止同一查询被重复执行if (this._execCount 0) { let str this.toString(); if (str.length 60) { str str.slice(0, 60) ...; } throw new MongooseError(Query was already executed: str); } this._execCount;从源码结构看查询对象一旦执行完毕无论通过.exec()还是.then()再次执行就会抛出Query was already executed错误。因此不要试图复用同一个查询对象每个查询应当单独构建。四、应该用exec()搭配await吗使用await处理查询有两种等价写法// 方式一直接 await 查询 await Band.findOne(); // 方式二await 查询的 exec() 结果 await Band.findOne().exec();从功能上看这两种方式是等价的。但Mongoose 官方强烈推荐使用.exec()核心原因是它能带来更好的堆栈追踪stack trace。文档用真实对比展示了这一差异——当传入一个非法 ObjectId 时const badId this is not a valid id; try { await Band.findOne({ _id: badId }); } catch (err) { // 不使用 exec()堆栈中看不到你的调用代码 // CastError: Cast to ObjectId failed for value this is not a valid id at path _id for model band-promises // at new CastError (/app/node_modules/mongoose/lib/error/cast.js:29:11) // at model.Query.exec (/app/node_modules/mongoose/lib/query.js:4331:21) // at model.Query.Query.then (/app/node_modules/mongoose/lib/query.js:4423:15) // at process._tickCallback (internal/process/next_tick.js:68:7) err.stack; } try { await Band.findOne({ _id: badId }).exec(); } catch (err) { // 使用 exec()堆栈中明确包含你调用 exec() 的位置 // CastError: Cast to ObjectId failed for value this is not a valid id at path _id for model band-promises // at new CastError (/app/node_modules/mongoose/lib/error/cast.js:29:11) // at model.Query.exec (/app/node_modules/mongoose/lib/query.js:4331:21) // at Context.anonymous (/app/test/index.test.js:138:42) // at process._tickCallback (internal/process/next_tick.js:68:7) err.stack; }可以看到直接await查询时堆栈只到Query.then内部lib/query.js就结束了无法定位到业务代码的调用行而通过.exec()时堆栈会包含Context.anonymous (/app/test/index.test.js:138:42)这样的调用方位置排查问题时能直接跳到出错的那行业务代码。这一差异的底层原因同样在源码中查询对象本身不是 Promiseawait查询时需要先通过 thenable 解析机制间接执行而exec()是 lib/query.js 中定义的async function它返回的原生 Promise 会保留完整的异步调用上下文从而让 V8 引擎能够还原更完整的调用链。五、执行流程与测试验证exec()不仅仅是返回一个 Promise它还承载了完整的查询执行管线。从 lib/query.js 的实现看一次exec()调用依次经历操作校验_validateOp()检查查询op是否合法lib/query.js执行前钩子先触发 Kareem 层的execPre(exec)lib/query.js再执行_executePreHooks()lib/query.js包括validate、find等中间件核心操作执行通过opToThunk映射表调用对应的_this[thunk]()lib/query.js结果转换依次应用_transforms变换lib/query.js错误处理捕获错误后调用_transformDuplicateKeyError归一化重复键错误lib/query.js执行后钩子执行_executePostHooks()与execPost(exec)lib/query.js。这也是为什么查询中间件如pre(find)、post(save)能够生效的原因——它们都挂载在这条exec()管线之上。这些行为在仓库测试中有完整覆盖test/docs/promises.test.js 直接对应本文档的示例代码验证了.save()返回 Promise、查询不是 Promise、查询可 thenable 等全部断言此外 test/query.test.js、test/queryhelpers.test.js 等测试文件也大量使用.exec()编写异步断言可作为实际用法参考。六、实践建议总结场景推荐写法说明保存文档await doc.save()save()本身返回原生 Promise可直接 await单条查询await Model.findOne(...).exec()推荐 exec()堆栈更完整多条查询await Model.find(...).exec()同上可配合.lean()提升性能纯链式调用Model.findOne(...).then(...)查询是 thenable链式可用但出错难定位查询复用禁止查询对象只能执行一次否则抛Query was already executed错误捕获try/catch包裹 await注意 CastError 等类型化错误可配合instanceof判断核心结论.save()返回真正的 Promiselib/model.js 定义为 async 函数可直接await查询不是 Promise但是 thenable——.then()/.catch()/.finally()都是对exec()的委托包装lib/query.js优先使用.exec()await功能上与直接 await 等价但能获得包含业务调用位置的完整堆栈显著降低排障成本查询对象不可复用内部_execCount机制lib/query.js保证查询只能执行一次。掌握这套 Promise 语义你就能在任何 async/await 项目中放心地使用 Mongoose同时保留对错误来源的完整追踪能力。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →