
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),僅供參考