
在 Mongoose 中使用 Async/Await查询、Promise 与可等待对象的完整指南【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseMongoose 的查询、保存等操作全部是异步的而 async/await 是消除回调地狱、以同步风格编写异步代码的最直接手段。本文以 Mongoose 官方文档 docs/async-await.md 为主线结合仓库源码lib/query.js、lib/model.js与官方测试test/docs/promises.test.js系统讲解 async/await 在 Mongoose 中的三种典型用法、Mongoose Query 作为 thenable 的特殊语义以及查询被重复执行这一核心陷阱的成因与规避方法。读完本文你将能写出既简洁又安全、可完全掌控查询执行时机的异步数据访问代码。一、基本用法从 Promise 链到 async/awaitAsync/await 让我们可以像写同步代码一样书写异步代码。对于 Mongoose 这类以大量连续异步操作为主的库而言这一点尤为重要——它能把查记录 → 改字段 → 保存 → 打印结果这类多步流程从层层嵌套的then中解放出来。下面两个函数做的是同一件事从数据库中取出一个记录、修改它、再把更新后的结果打印到控制台。第一个版本使用 Promise 链式调用// Using promise chaining function thenUpdate() { MyModel.findOne({ firstName: franklin, lastName: roosevelt }) .then(function(doc) { doc.middleName delano; return doc.save(); }) .then(console.log) .catch(function(err) { handleError(err); }); }第二个版本使用 async/await逻辑完全一致但结构上是扁平的// Using async/await async function awaitUpdate() { try { const doc await MyModel.findOne({ firstName: franklin, lastName: roosevelt }); doc.middleName delano; console.log(await doc.save()); } catch (err) { handleError(err); } }两个版本的关键差异在于错误处理与流程控制Promise 链版本中每一环都必须显式return否则下一步拿到的将是undefinedasync/await 版本中await之后的代码天然按顺序执行异常统一由try/catch捕获不再需要逐环传递.catch从源码角度看doc.save()与查询方法返回的对象类型不同详见下文查询与 Promise 的区别一节这决定了await作用于两者时的行为差异。需要特别说明的是不同 Mongoose 方法的履行值fulfillment value各不相同且可能受配置影响。例如findOne()解析为单个文档或nullfind()解析为文档数组save()解析为保存后的文档本身见 lib/model.js 中Model.prototype.save的return _this;。具体方法的返回语义请查阅 API 文档 docs/api.md。二、async 函数始终返回 Promise给 JavaScript 函数加上async关键字后该函数无论函数体里写了什么 return 语句返回值都会被包装成一个原生的 JavaScript Promise。这是 async 函数的基本语义也是初学者最容易踩的第一个坑async function getUser() { // Inside getUser, we can await an async operation and interact with // foundUser as a normal, non-promise value... const foundUser await User.findOne({ name: bill }); console.log(foundUser); // Prints {name: bill, admin: false} return foundUser; } // However, because async functions always return a promise, // user is a promise. const user getUser(); console.log(user); // Oops. Prints [Promise]上面的user不是用户文档而是一个 Promise因此直接console.log只能看到[Promise]。正确做法是把 async 函数的返回值当成普通 Promise 来处理——要么在另一个 async 函数中await它要么用.then链式消费它async function getUser() { const foundUser await User.findOne({ name: bill }); return foundUser; } async function doStuffWithUser() { // Await the promise returned from calling getUser. const user await getUser(); console.log(user); // Prints {name: bill, admin: false} }这个例子的意义在于async 函数是 Promise 生态的一部分它内部可以await任何 Promise包括 Mongoose 的查询与保存对外则以统一 Promise 的形式暴露结果。这也意味着你可以把 Mongoose 操作封装成 async 函数再在 Express 路由、测试用例等任意 async 上下文中消费它。三、Async/Await 与 Mongoose 查询thenable 与 Promise 的本质区别从语言层面看async/await 只是 Promise API 之上的语法糖。JavaScript 的await关键字会尝试解包任何带有名为then、值为函数属性的对象——这类对象统称为 thenable。如果解包的是真正的 Promise即Promise构造函数的实例我们可以获得关于then行为的一系列保证但 Mongoose 的多个静态辅助方法如find()、findOne()、findById()返回的并不是 Promise而是一种特殊的 thenable 对象——Query。Query 不是 Promise详见 docs/queries.md 中 Queries are not promises 一节。因为 Query 也是 thenable所以await一个查询与await一个真正的 Promise 在写法上并无二致但二者存在一个决定性的行为差异观察一个真正 Promise 的履行值无论如何都不可能改变该值本身但重复观察同一个 Query 的履行值可能导致该查询被重新执行。用instanceof Promise验证身份function isPromise(thenable) { return thenable instanceof Promise; } // The fulfillment value of the promise returned by user.save() will always be the same, // regardless of how, or how often, we observe it. async function observePromise() { const user await User.findOne({ firstName: franklin, lastName: roosevelt }); user.middleName delano; // Document.prototype.save() returns a *genuine* promise const realPromise user.save(); console.log(isPromise(realPromise)); // true const awaitedValue await realPromise; realPromise.then(chainedValue console.log(chainedValue awaitedValue)); // true } // By contrast, the value we receive when we try to observe the same Query more than // once is different every time. The Query is re-executing. async function observeQuery() { const query User.findOne({ firstName: leroy, lastName: jenkins }); console.log(isPromise(query)); // false const awaitedValue await query; query.then(chainedValue console.log(chainedValue awaitedValue)); // false }源码层面的验证这段行为差异在仓库源码中有直接体现。查询执行入口 lib/query.js 的Query.prototype.exec是一个async函数它内部用_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;也就是说同一 Query 对象被提交执行真正发出数据库请求最多只有一次。而 Query 的 thenable 实现如下lib/query.jsQuery.prototype.then function(resolve, reject) { return this.exec().then(resolve, reject); }; Query.prototype.catch function(reject) { return this.exec().then(null, reject); }; Query.prototype.finally function(onFinally) { return this.exec().finally(onFinally); };then/catch/finally都直接调用exec()。因此每次对 Query 调用.thenawait底层正是调用.then都会触发一次exec()而重复触发exec()会抛出MongooseError: Query was already executed——这正是 observeQuery 示例中两次观察得到不同结果、且第二次以报错告终的根源。与之对照Model.prototype.savelib/model.js是一个async function返回的是真正的原生 Promise无论被await、被.then消费多少次其履行值都恒定不变。四、最容易踩坑的场景回调与 async/await 混用官方文档特别指出最可能在将回调风格与 async/await 混用时无意间重复执行查询例如// 反面示例query 既是 thenable 又被回调消费极易重复执行 const query User.findOne({ name: bill }); query.then(doc { /* ... */ }); const doc await query; // 再次触发 exec()抛错或产生意外行为这种写法从来都不是必须的应当避免。设计原则是一个查询只消费一次。如果你确实需要把一个 Query 变成货真价实的 Promise例如要传给只接受 Promise 的第三方工具可以使用 Query#exec()const promise User.findOne({ name: bill }).exec(); // 返回真正的 Promise需要说明的是在 lib/query.js 中exec()已经不再接受回调参数传入回调会直接抛出MongooseError它的唯一职责就是执行查询并返回一个 Promise。五、await query还是await query.exec()这是使用 async/await 时最常见的二选一问题。官方测试 test/docs/promises.test.js 给出了明确结论const doc await Band.findOne({ name: Guns N\ Roses }); // works // 与下面这行功能上等价 const doc await Band.findOne({ name: Guns N\ Roses }).exec();功能上两者完全等价await Band.findOne()更简洁await Band.findOne().exec()更显式且能确保你await的一定是货真价实的 Promise历史背景Mongoose 6 时代官方推荐使用.exec()因为它在类型转换失败等错误发生时能提供更好的调用栈stack trace到了 Mongoose 7V8 引擎的 async stack traces 已经足够完善两者在堆栈信息上的差异不复存在因此你可以按个人偏好任选其一。测试中还展示了实际堆栈差异不带exec()时CastError的堆栈止于Query.then带exec()时堆栈会包含你在代码中调用exec()的位置更便于定位问题。若你的环境不支持 V8 async stack traces如 Deno则仍需依赖.exec()获取完整调用栈。六、最佳实践小结结合以上分析与源码证据在使用 async/await 编写 Mongoose 代码时建议遵循以下规则函数体加async前先想清楚async 函数必然返回 Promise调用方必须await或.then消费它一个查询只观察一次不要对同一个 Query 对象既await又.then或多次await以免触发 lib/query.js 中的重复执行保护逻辑需要真 Promise 时调用.exec()Query.prototype.exec返回原生 Promise适合与 Promise.all、async 工具库等协作区分履行值语义findOne()解析为文档或nullfind()解析为文档数组save()解析为保存后的文档本身多文档操作的解析结果可参考 docs/queries.md把try/catch作为默认错误策略async/await 版本中所有异步异常都会汇聚到最近的catch无需像 Promise 链那样逐环注册拒绝处理器保持单一风格不要在同一个流程中混用回调、Promise 链与 async/await这是官方文档中明确指出、也是最容易诱发查询重复执行的写法。Async/await 与 Promise 是 Mongoose 异步模型的基石Query作为 thenable 提供了顺滑的await体验而exec()与真正的 Promise如save()的返回值则提供了确定性的观察语义。理解二者的边界就能既享受 async/await 的简洁又避开查询被重复执行的隐性陷阱。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考