使用 Lean 加速 Mongoose 查询
The lean 选项 tells Mongoose to skip hydrating the result documents. This makes queries faster and less memory intensive, but the result documents are plain old JavaScript objects (POJOs), not Mongoose documents. In this tutorial, you'll learn more about the tradeoffs of using 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 文档没有
- 更改跟踪
- 转换和验证
- getter 和 setter
- 虚拟属性
save()
例如,以下代码示例显示,如果您启用 lean
,则 Person
模型的 getter 和虚拟属性不会运行。
// 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 和填充
填充 与 lean()
协同工作。如果您同时使用 populate()
和 lean()
,则 lean
选项也会传播到填充的文档。在下面的示例中,顶级 'Group' 文档和填充的 '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
虚拟填充 也与 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。通常情况下,如果您不修改查询结果,也不使用 自定义 getter,则应使用 lean()
。如果您修改查询结果或依赖于 getter 或 转换 等功能,则不应使用 lean()
。
以下是一个 Express 路由 的示例,它适合使用 lean()
。此路由不会修改 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 }));
});
以下是一个 Express 路由的示例,它不应该使用 lean()
。作为一般规则,GET
路由是 RESTful API 中使用 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 功能,包括 虚拟属性、getter/setter 和 默认值。如果您想在 lean()
中使用这些功能,您需要使用相应的插件
但是,您需要记住,Mongoose 不会填充 Lean 文档,因此 this
在虚拟属性、getter 和默认函数中将是一个 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.
});
BigInts
默认情况下,MongoDB Node 驱动程序将存储在 MongoDB 中的长整型转换为 JavaScript 数字,而不是 BigInt。在您的 lean()
查询中设置 useBigInt64
选项以将长整型转换为 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'