查詢

Mongoose 模型提供了幾個靜態輔助函式,用於 CRUD 操作。這些函式中的每一個都會回傳一個 mongoose Query 物件

Mongoose 查詢可以透過兩種方式執行。首先,如果你傳入一個 callback 函式,Mongoose 將會非同步執行查詢,並將結果傳遞給 callback

查詢也有一個 .then() 函式,因此可以作為一個 Promise 使用。

執行

在執行查詢時,您會將查詢指定為 JSON 文檔。JSON 文檔的語法與 MongoDB shell 的語法相同。

const Person = mongoose.model('Person', yourSchema);

// find each person with a last name matching 'Ghost', selecting the `name` and `occupation` fields
const person = await Person.findOne({ 'name.last': 'Ghost' }, 'name occupation');
// Prints "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() 是一個 文檔列表count()文檔的數量update()受影響的文檔數量 等等。模型 API 文件提供了更多詳細資訊。

現在讓我們看看在沒有使用 await 的情況下會發生什麼

// find each person with a last name matching 'Ghost'
const query = Person.findOne({ 'name.last': 'Ghost' });

// selecting the `name` and `occupation` fields
query.select('name occupation');

// execute the query at a later time
const person = await query.exec();
// Prints "Space Ghost is a talk show host."
console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation);

在上面的程式碼中,query 變數的類型是 QueryQuery 使您能夠使用鏈式語法來建立查詢,而不是指定 JSON 物件。以下兩個範例是等效的。

// With a JSON doc
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();

// Using query builder
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();

API 文件中可以找到 Query 輔助函式的完整列表

查詢不是 Promise

Mongoose 查詢不是 Promise。查詢是 thenable,表示它們具有 .then() 方法,為了方便起見可作為 async/await 使用。然而,與 Promise 不同的是,呼叫查詢的 .then() 會執行查詢,因此多次呼叫 then() 將會拋出錯誤。

const q = MyModel.updateMany({}, { isDeleted: true });

await q.then(() => console.log('Update 2'));
// Throws "Query was already executed: Test.updateMany({}, { isDeleted: true })"
await q.then(() => console.log('Update 3'));

參考其他文檔

MongoDB 中沒有關聯 (joins),但有時我們仍然需要參考其他集合中的文檔。這就是 填充 的用武之地。請在這裡閱讀更多關於如何在查詢結果中包含來自其他集合的文檔的資訊。

串流

您可以從 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); // Prints documents one at a time
}

使用 非同步迭代器迭代 Mongoose 查詢也會建立一個游標。

for await (const doc of Person.find()) {
  console.log(doc); // Prints documents one at a time
}

游標會受到游標逾時的影響。預設情況下,MongoDB 將會在 10 分鐘後關閉您的游標,後續的 next() 呼叫將會導致 MongoServerError: cursor id 123 not found 錯誤。要覆寫此行為,請在游標上設定 noCursorTimeout 選項。

// MongoDB won't automatically close this cursor after 10 minutes.
const cursor = Person.find().cursor().addCursorFlag('noCursorTimeout', true);

然而,游標仍然可能會因為會話閒置逾時而逾時。因此,即使設定了 noCursorTimeout 的游標,仍然會在閒置 30 分鐘後逾時。您可以在 MongoDB 文件中閱讀更多關於解決會話閒置逾時問題的資訊。

與聚合的比較

聚合可以完成查詢可以完成的許多相同事情。例如,以下是如何使用 aggregate() 來尋找 name.last = 'Ghost' 的文檔

const docs = await Person.aggregate([{ $match: { 'name.last': 'Ghost' } }]);

然而,僅僅因為您可以使用 aggregate() 並不意味著您應該使用它。一般來說,您應該盡可能使用查詢,只有在絕對需要時才使用 aggregate()

與查詢結果不同,Mongoose 不會 hydrate() 聚合結果。聚合結果始終是 POJO,而不是 Mongoose 文檔。

const docs = await Person.aggregate([{ $match: { 'name.last': 'Ghost' } }]);

docs[0] instanceof mongoose.Document; // false

此外,與查詢篩選器不同的是,Mongoose 也不轉換聚合管道。這表示您有責任確保傳遞給聚合管道的值具有正確的類型。

const doc = await Person.findOne();

const idString = doc._id.toString();

// Finds the `Person`, because Mongoose casts `idString` to an ObjectId
const queryRes = await Person.findOne({ _id: idString });

// Does **not** find the `Person`, because Mongoose doesn't cast aggregation
// pipelines.
const aggRes = await Person.aggregate([{ $match: { _id: idString } }]);

排序

排序是確保您的查詢結果以所需順序回傳的方式。

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 }); // returns age starting from 10 as the first entry
await Person.find().sort({ age: 1 }); // returns age starting from 0 as the first entry

當使用多個欄位進行排序時,排序鍵的順序決定了 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 }); // returns age starting from 0, but while keeping that order will then sort by weight.

您可以在下面檢視此程式碼區塊單次執行的輸出。如您所見,年齡從 0 到 2 排序,但是當年齡相等時,則按權重排序。

[
  {
    _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
  }
];

接下來

現在我們已經介紹了 Queries,讓我們看看驗證