Lean を使用した高速な Mongoose クエリ

lean オプション は、Mongoose に結果ドキュメントの ハイドレーション をスキップするよう指示します。これにより、クエリは高速になり、メモリ消費も少なくなりますが、結果ドキュメントはプレーンな JavaScript オブジェクト (POJO) となり、Mongoose ドキュメント になりません。このチュートリアルでは、lean() を使用することのトレードオフについて詳しく説明します。

Lean の使用

デフォルトでは、Mongoose クエリは Mongoose の Document クラス のインスタンスを返します。ドキュメントは変更追跡のための多くの内部状態を持つため、プレーンな JavaScript オブジェクトよりもはるかに重くなります。lean オプションを有効にすると、Mongoose は完全な Mongoose ドキュメントのインスタンス化をスキップし、POJO を返します。

const leanDoc = await MyModel.findOne().lean();

Lean ドキュメントはどのくらい小さくなりますか? 以下に比較を示します。

const schema = new mongoose.Schema({ name: String });
const MyModel = mongoose.model('Test', schema);

await MyModel.create({ name: 'test' });

const normalDoc = await MyModel.findOne();
// To enable the `lean` option for a query, use the `lean()` function.
const leanDoc = await MyModel.findOne().lean();

v8Serialize(normalDoc).length; // approximately 180
v8Serialize(leanDoc).length; // approximately 55, about 3x smaller!

// In case you were wondering, the JSON form of a Mongoose doc is the same
// as the POJO. This additional memory only affects how much memory your
// Node.js process uses, not how much data is sent over the network.
JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // true

内部的には、クエリの実行後、Mongoose はクエリ結果を POJO から Mongoose ドキュメントに変換します。lean オプションを有効にすると、Mongoose はこのステップをスキップします。

const normalDoc = await MyModel.findOne();
const leanDoc = await MyModel.findOne().lean();

normalDoc instanceof mongoose.Document; // true
normalDoc.constructor.name; // 'model'

leanDoc instanceof mongoose.Document; // false
leanDoc.constructor.name; // 'Object'

lean を有効にすることの欠点は、Lean ドキュメントには以下のものが含まれないことです。

  • 変更追跡
  • キャストとバリデーション
  • ゲッターとセッター
  • 仮想プロパティ
  • save()

たとえば、次のコードサンプルは、lean を有効にすると、Person モデルのゲッターと仮想プロパティが実行されないことを示しています。

// Define a `Person` model. Schema has 2 custom getters and a `fullName`
// virtual. Neither the getters nor the virtuals will run if lean is enabled.
const personSchema = new mongoose.Schema({
  firstName: {
    type: String,
    get: capitalizeFirstLetter
  },
  lastName: {
    type: String,
    get: capitalizeFirstLetter
  }
});
personSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});
function capitalizeFirstLetter(v) {
  // Convert 'bob' -> 'Bob'
  return v.charAt(0).toUpperCase() + v.substring(1);
}
const Person = mongoose.model('Person', personSchema);

// Create a doc and load it as a lean doc
await Person.create({ firstName: 'benjamin', lastName: 'sisko' });
const normalDoc = await Person.findOne();
const leanDoc = await Person.findOne().lean();

normalDoc.fullName; // 'Benjamin Sisko'
normalDoc.firstName; // 'Benjamin', because of `capitalizeFirstLetter()`
normalDoc.lastName; // 'Sisko', because of `capitalizeFirstLetter()`

leanDoc.fullName; // undefined
leanDoc.firstName; // 'benjamin', custom getter doesn't run
leanDoc.lastName; // 'sisko', custom getter doesn't run

Lean と Populate

Populatelean() と連携します。populate()lean() の両方を使用する場合、lean オプションは populate されたドキュメントにも適用されます。以下の例では、最上位の 'Group' ドキュメントと populate された 'Person' ドキュメントの両方が lean になります。

// Create models
const Group = mongoose.model('Group', new mongoose.Schema({
  name: String,
  members: [{ type: mongoose.ObjectId, ref: 'Person' }]
}));
const Person = mongoose.model('Person', new mongoose.Schema({
  name: String
}));

// Initialize data
const people = await Person.create([
  { name: 'Benjamin Sisko' },
  { name: 'Kira Nerys' }
]);
await Group.create({
  name: 'Star Trek: Deep Space Nine Characters',
  members: people.map(p => p._id)
});

// Execute a lean query
const group = await Group.findOne().lean().populate('members');
group.members[0].name; // 'Benjamin Sisko'
group.members[1].name; // 'Kira Nerys'

// Both the `group` and the populated `members` are lean.
group instanceof mongoose.Document; // false
group.members[0] instanceof mongoose.Document; // false
group.members[1] instanceof mongoose.Document; // false

仮想プロパティの Populate も lean と連携します。

// Create models
const groupSchema = new mongoose.Schema({ name: String });
groupSchema.virtual('members', {
  ref: 'Person',
  localField: '_id',
  foreignField: 'groupId'
});
const Group = mongoose.model('Group', groupSchema);
const Person = mongoose.model('Person', new mongoose.Schema({
  name: String,
  groupId: mongoose.ObjectId
}));

// Initialize data
const g = await Group.create({ name: 'DS9 Characters' });
await Person.create([
  { name: 'Benjamin Sisko', groupId: g._id },
  { name: 'Kira Nerys', groupId: g._id }
]);

// Execute a lean query
const group = await Group.findOne().lean().populate({
  path: 'members',
  options: { sort: { name: 1 } }
});
group.members[0].name; // 'Benjamin Sisko'
group.members[1].name; // 'Kira Nerys'

// Both the `group` and the populated `members` are lean.
group instanceof mongoose.Document; // false
group.members[0] instanceof mongoose.Document; // false
group.members[1] instanceof mongoose.Document; // false

Lean を使用する場面

クエリを実行し、変更せずに結果を、たとえば Express レスポンス に送信する場合、lean を使用する必要があります。一般的に、クエリ結果を変更せず、カスタムゲッター を使用しない場合は、lean() を使用する必要があります。クエリ結果を変更するか、ゲッターや トランスフォーム などの機能に依存する場合は、lean() を使用しないでください。

以下は、lean() の適切な候補となる Express ルート の例です。このルートは person ドキュメントを変更せず、Mongoose 固有の機能には依存しません。

// As long as you don't need any of the Person model's virtuals or getters,
// you can use `lean()`.
app.get('/person/:id', function(req, res) {
  Person.findOne({ _id: req.params.id }).lean().
    then(person => res.json({ person })).
    catch(error => res.json({ error: error.message }));
});

以下は、lean() を使用すべきではない Express ルートの例です。一般則として、RESTful API では、GET ルートは lean() の適切な候補です。一方、PUTPOST などのルートは一般的に lean() を使用すべきではありません。

// This route should **not** use `lean()`, because lean means no `save()`.
app.put('/person/:id', function(req, res) {
  Person.findOne({ _id: req.params.id }).
    then(person => {
      assert.ok(person);
      Object.assign(person, req.body);
      return person.save();
    }).
    then(person => res.json({ person })).
    catch(error => res.json({ error: error.message }));
});

仮想プロパティは lean() クエリ結果には含まれないことに注意してください。mongoose-lean-virtuals プラグイン を使用して、lean クエリ結果に仮想プロパティを追加できます。

プラグイン

lean() を使用すると、仮想プロパティゲッター/セッターデフォルト値 などのすべての Mongoose 機能がバイパスされます。lean() でこれらの機能を使用する場合は、対応するプラグインを使用する必要があります。

ただし、Mongoose は lean ドキュメントをハイドレートしないため、仮想プロパティ、ゲッター、デフォルト関数の this は POJO になることに注意する必要があります。

const schema = new Schema({ name: String });
schema.plugin(require('mongoose-lean-virtuals'));

schema.virtual('lowercase', function() {
  this instanceof mongoose.Document; // false

  this.name; // Works
  this.get('name'); // Crashes because `this` is not a Mongoose document.
});

BigInt

デフォルトでは、MongoDB Node ドライバーは MongoDB に格納されている long を JavaScript の数値に変換し、BigInt にはしません。lean() クエリで useBigInt64 オプションを設定して、long を BigInt に変換します。

const Person = mongoose.model('Person', new mongoose.Schema({
  name: String,
  age: BigInt
}));
// Mongoose will convert `age` to a BigInt
const { age } = await Person.create({ name: 'Benjamin Sisko', age: 37 });
typeof age; // 'bigint'

// By default, if you store a document with a BigInt property in MongoDB and you
// load the document with `lean()`, the BigInt property will be a number
let person = await Person.findOne({ name: 'Benjamin Sisko' }).lean();
typeof person.age; // 'number'

// Set the `useBigInt64` option to opt in to converting MongoDB longs to BigInts.
person = await Person.findOne({ name: 'Benjamin Sisko' }).
  setOptions({ useBigInt64: true }).
  lean();
typeof person.age; // 'bigint'