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
Populate は lean()
と連携します。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()
の適切な候補です。一方、PUT
、POST
などのルートは一般的に 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'