Mongoose Queries 实战指南从 Model 查询方法到 Query 构建器、游标流式读取与排序【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseMongoose 的 Model 提供了find()、findOne()、updateOne()、deleteMany()等一系列静态辅助函数用于对 MongoDB 集合执行 CRUD 操作且每一个函数都会返回一个 mongooseQuery对象。本文以仓库中的官方指南 docs/queries.md 为骨架结合 lib/query.js 与 lib/cursor/queryCursor.js 的源码实现系统讲解 Query 的两种执行方式、链式构建器、thenable 陷阱、引用填充、游标流式读取、与聚合管道的差异以及多字段排序帮助你写出可预测、可调试、性能正确的查询代码。Model 的查询静态方法一览Mongoose models 提供以下静态辅助函数每个函数返回一个 mongooseQuery对象Model.deleteMany()Model.deleteOne()Model.find()Model.findById()Model.findByIdAndDelete()Model.findByIdAndRemove()Model.findByIdAndUpdate()Model.findOne()Model.findOneAndDelete()Model.findOneAndReplace()Model.findOneAndUpdate()Model.replaceOne()Model.updateMany()Model.updateOne()在源码层面这些静态方法最终都会构造一个 Query 实例并执行。以find()为例lib/query.js#L2547-L2564 中Query.prototype.find会设置this.op find然后通过merge(conditions)合并查询条件Query.prototype.findOnelib/query.js#L2829则会额外处理投影与选项参数。Query 构造函数lib/query.js#L116内部维护了_transforms、_hooksKareem 中间件容器与_execCount记录执行次数这些字段是理解下文Query 不是 Promise的关键。执行查询的两种方式执行查询时你将查询条件写成 JSON 文档其语法与 MongoDB shell 完全一致const Person mongoose.model(Person, yourSchema); // 查找姓氏为 Ghost 的人只选择 name 和 occupation 字段 const person await Person.findOne({ name.last: Ghost }, name occupation); // 输出 Space Ghost is a talk show host console.log(%s %s is a %s., person.name.first, person.name.last, person.occupation);person的形态取决于具体操作findOne()返回一个可能为 null 的单个文档find()返回文档列表countDocuments()返回文档数量updateOne()返回受影响文档数等。更多细节见 Model 的 API 文档。延迟执行先用 Query 构建再手动 exec()如果暂不await你会得到一个尚未执行的 Query// 查找姓氏为 Ghost 的人 const query Person.findOne({ name.last: Ghost }); // 选择 name 和 occupation 字段 query.select(name occupation); // 在之后的某个时刻执行该查询 const person await query.exec(); // 输出 Space Ghost is a talk show host console.log(%s %s is a %s., person.name.first, person.name.last, person.occupation);上面的query变量类型是 Query。它允许你用链式语法逐步构建查询而不是一次性给出完整的 JSON 对象。从源码看Query.prototype.execlib/query.js#L4744-L4761会先做三项校验不再接受回调函数传入函数会直接抛出Query.prototype.exec() no longer accepts a callback、校验操作类型op与关联的model是否为空随后通过opToThunk表查找到对应操作的执行函数。这也解释了为什么先构建、后执行的延迟模式是可行的——Query 对象本身只是条件与选项的容器。JSON 文档写法与 Query 构建器写法等价下面两个例子完全等价你可以按场景自由选择// 方式一一次性传入 JSON 文档 await Person. find({ occupation: /host/, name.last: Ghost, age: { $gt: 17, $lt: 66 }, likes: { $in: [vaporizing, talking] } }). limit(10). sort({ occupation: -1 }). select({ name: 1, occupation: 1 }). exec(); // 方式二使用 Query 构建器链式组装 await Person. find({ occupation: /host/ }). where(name.last).equals(Ghost). where(age).gt(17).lt(66). where(likes).in([vaporizing, talking]). limit(10). sort(-occupation). select(name occupation). exec();构建器模式由一组可链式调用的方法支撑。在 lib/query.js 中可以看到它们的实现轮廓limit(v)lib/query.js#L932会把值写入this.options.limit并返回thisselect()lib/query.js#L1128内部通过parseProjection解析字段投影并支持sanitizeProjection选项sort(arg, options)lib/query.js#L3135最多接受 2 个参数并将排序写入this.options.sort。所有方法都返回this从而形成链式调用。完整的方法清单见 Query 的 API 文档。Queries 不是 Promise但它们是 thenableMongoose 的 Query不是Promise。它们只是 thenable拥有.then()方法的对象为async/await提供便利。关键在于与 Promise 不同调用 Query 的.then()会真正执行查询因此对同一个 Query 多次调用then()会抛出错误。const q MyModel.updateMany({}, { isDeleted: true }); await q.then(() console.log(Update 2)); // 抛出 Query was already executed: Test.updateMany({}, { isDeleted: true }) await q.then(() console.log(Update 3));源码给出了直接证据。lib/query.js#L4901-L4934 中三个方法都通过exec()触发真正的执行Query.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); };也就是说await query、query.then(...)、query.catch(...)、query.finally(...)都会各自触发一次查询执行。如果想要安全的重复使用查询条件请保留原始 Query 并在每次执行前通过链式方法复制/重建或者直接对同一条件对象多次调用 Model 静态方法而不是复用同一个 Query 实例。引用其他文档PopulationMongoDB 没有 join但有时我们仍然希望查询结果中能包含其他集合中文档的引用。这正是 population填充 的用武之地。关于如何在查询结果中引入其他集合的文档详见 Query#populate 的 API 文档。Population 是查询阶段的可选步骤先执行原始查询拿到主文档再根据ref与本地外键字段批量发起对目标集合的二次查询从而在业务层面模拟出关联查询的效果。流式读取Query#cursor() 与 QueryCursor你可以从 MongoDB流式读取查询结果。需要调用 Query#cursor() 获取一个 QueryCursor 实例const cursor Person.find({ occupation: /host/ }).cursor(); for (let doc await cursor.next(); doc ! null; doc await cursor.next()) { console.log(doc); // 逐条打印文档 }源码层面Query.prototype.cursorlib/query.js#L5368-L5386在创建游标前会先调用_castConditions()进行条件转换若转换失败例如过滤器含有sanitizeFilter拒绝的$where会返回一个标记了错误的 QueryCursor否则返回new QueryCursor(this)。QueryCursor.prototype.nextlib/cursor/queryCursor.js#L307-L333同样不再接受回调内部以 Promise 形式逐条取文档并对已关闭的游标调用next()抛出Cannot call next() on a closed cursor。使用 async iterators 遍历使用 async iterators 遍历 Mongoose 查询也会自动创建游标for await (const doc of Person.find()) { console.log(doc); // 逐条打印文档 }在 lib/cursor/queryCursor.js#L434-L440 中可以看到实现当Symbol.asyncIterator存在时QueryCursor.prototype[Symbol.asyncIterator]会设置_mongooseOptions._asyncIterator true并返回自身后续_next回调会把结果包装成{ value, done }形式lib/cursor/queryCursor.js#L476-L483。测试 test/query.test.js 中也有对应的遍历用例例如使用for await消费经过transform()处理后的游标结果。游标超时与 noCursorTimeout游标受游标超时约束。默认情况下MongoDB 会在 10 分钟后关闭游标之后的next()调用会抛出MongoServerError: cursor id 123 not found。要覆盖这一行为请为游标设置noCursorTimeout选项// MongoDB 不会在 10 分钟后自动关闭该游标 const cursor Person.find().cursor().addCursorFlag(noCursorTimeout, true);不过游标仍然可能因为会话空闲超时session idle timeouts而失效即使设置了noCursorTimeout游标在空闲 30 分钟后依然会超时。这在 MongoDB 官方文档中也有明确说明cursor.noCursorTimeout一节。因此对于长时间运行的批处理任务更稳妥的做法是控制单批处理时长、及时关闭游标或将数据按时间片拆分查询而不是无限期依赖noCursorTimeout。何时使用 aggregate()Queries vs AggregationAggregation聚合 能做很多查询能做的事情。例如下面是用aggregate()查找name.last Ghost的文档const docs await Person.aggregate([{ $match: { name.last: Ghost } }]);但能用不等于应该用。一般来说能用普通查询就优先用查询只有确实需要时才使用aggregate()。两者的关键差异有三点1. 聚合结果不做 hydrate与查询结果不同Mongoose不会对聚合结果调用hydrate()。聚合结果永远是普通对象POJO而不是 Mongoose 文档const docs await Person.aggregate([{ $match: { name.last: Ghost } }]); docs[0] instanceof mongoose.Document; // false这意味着聚合结果没有 Mongoose 文档的实例方法、getter/setter、虚拟字段与修改追踪能力如果你需要这些能力必须对结果自行 hydrate 或做二次查询。2. 聚合管道不做类型转换cast与查询过滤器不同Mongoose不会 cast类型转换 聚合管道。也就是说你必须自己保证传入聚合管道的值类型正确const doc await Person.findOne(); const idString doc._id.toString(); // 能查到这个 Person因为 Mongoose 把 idString 转换成了 ObjectId const queryRes await Person.findOne({ _id: idString }); // 查不到这个 Person因为 Mongoose 不转换聚合管道中的类型 const aggRes await Person.aggregate([{ $match: { _id: idString } }]);这是实践中非常容易踩坑的地方查询条件中的字符串 ObjectId 会被自动 cast而聚合管道的$match则不会。从源码结构看查询条件在 lib/query.js 的_castConditions/castFilterPath链路中会基于 Schema 类型逐路径转换而 lib/aggregate.js 对管道阶段默认不做同样的 Schema 级 cast。因此在使用聚合时请先用mongoose.Types.ObjectId(...)等构造函数显式转换类型。3. 关于 type casting 的进一步阅读想深入了解 Mongoose 对查询条件、更新条件与聚合条件的类型转换规则包括字符串化 ObjectId、数字与日期的隐式转换边界请阅读 查询类型转换指南。排序保证结果顺序可控Sorting 用于确保查询结果按期望的顺序返回const personSchema new mongoose.Schema({ age: Number }); const Person mongoose.model(Person, personSchema); for (let i 0; i 10; i) { await Person.create({ age: i }); } await Person.find().sort({ age: -1 }); // 返回结果以 age10 开头 await Person.find().sort({ age: 1 }); // 返回结果以 age0 开头-1表示降序1表示升序也可以使用字符串形式sort(-age)/sort(age)这也是上文构建器示例中sort(-occupation)的写法。多字段排序键的顺序决定优先级多字段排序时排序键的书写顺序决定了 MongoDB 服务端先按哪个字段排序const personSchema new mongoose.Schema({ age: Number, name: String, weight: Number }); const Person mongoose.model(Person, personSchema); const iterations 5; for (let i 0; i iterations; i) { await Person.create({ age: Math.abs(2 - i), name: Test i, weight: Math.floor(Math.random() * 100) 1 }); } await Person.find().sort({ age: 1, weight: -1 }); // 先按 age 升序age 相同时再按 weight 降序下面是一次实际运行的输出可以看到 age 从 0 升到 2而在 age 相同的记录之间则按 weight 降序排列[ { _id: new ObjectId(63a335a6b9b6a7bfc186cb37), age: 0, name: Test2, weight: 67, __v: 0 }, { _id: new ObjectId(63a335a6b9b6a7bfc186cb35), age: 1, name: Test1, weight: 99, __v: 0 }, { _id: new ObjectId(63a335a6b9b6a7bfc186cb39), age: 1, name: Test3, weight: 73, __v: 0 }, { _id: new ObjectId(63a335a6b9b6a7bfc186cb33), age: 2, name: Test0, weight: 65, __v: 0 }, { _id: new ObjectId(63a335a6b9b6a7bfc186cb3b), age: 2, name: Test4, weight: 62, __v: 0 } ];排序在 lib/query.js#L3135 的sort()实现中被写入this.options.sort最终以 MongoDB 排序规范{ field: 1|-1 }对象或field -field字符串下发给驱动。若需跨字段稳定排序请始终显式给出完整键序列不要依赖数据库的自然顺序。小结与下一步Mongoose Query 的核心心法可以概括为四点两种写法等价一次性 JSON 文档 vs 链式 Query 构建器选一种并保持一致Query 是 thenable 而非 Promise.then()/await都会执行查询不要重复调用同一个 Query大数据量用游标cursor()for await逐条处理注意 10 分钟默认超时与 30 分钟会话空闲超时的边界查询优先于聚合聚合不 hydrate、不 cast能写查询就写查询。接下来可以继续阅读 Validation校验学习如何在查询与文档保存前定义数据校验规则。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考