クエリ

Mongoose のモデルは、CRUD操作のためのいくつかの静的ヘルパー関数を提供します。これらの関数はそれぞれ、mongoose Query オブジェクトを返します。

mongoose クエリは、2つの方法のいずれかで実行できます。まず、callback 関数を渡すと、Mongoose はクエリを非同期に実行し、結果を callback に渡します。

クエリには .then() 関数もあり、プロミスとして使用できます。

実行

クエリを実行する際には、クエリを JSON ドキュメントとして指定します。JSON ドキュメントの構文は、MongoDB シェルと同じです。

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 変数は Query 型です。Query を使用すると、JSON オブジェクトを指定するのではなく、チェイン構文を使用してクエリを構築できます。以下の 2 つの例は同等です。

// 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ドキュメントにあります

クエリはプロミスではない

Mongoose クエリは、プロミス**ではありません**。クエリはthenableであり、利便性のためにasync/await用の .then() メソッドがあります。ただし、プロミスとは異なり、クエリの .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 には結合はありませんが、他のコレクションのドキュメントへの参照が必要になる場合があります。そこで、ポピュレーションが登場します。クエリ結果に他のコレクションのドキュメントを含める方法については、こちらをご覧ください。

ストリーミング

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
}

async イテレーターを使用して 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
  }
];

次へ

これで クエリについて説明したので、次にバリデーションを見てみましょう。