Mongoose `.lean()` 完全指南:跳过文档水合,让查询更快、内存占用更小
Mongoose.lean()完全指南跳过文档水合让查询更快、内存占用更小【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseMongoose 查询默认返回完整的Document实例带有变更追踪、校验等重状态而.lean()选项可以让查询直接返回纯 JavaScript 对象POJO从而显著提升查询速度并降低内存占用。本篇指南以 docs/tutorials/lean.md 为骨架结合本仓库 lib/query.js 与 lib/model.js 的源码实现系统讲解lean()的使用方式、与populate()的配合、适用场景、插件生态以及 BigInt 处理读完即可在实际项目中安全、高效地使用lean()优化只读查询。什么是lean选项从「Mongoose 文档」到「纯 JavaScript 对象」lean 选项告诉 Mongoose 跳过对结果文档的水合hydrate过程。水合是指查询执行完毕后Mongoose 把 MongoDB 返回的普通对象转换为 MongooseDocument实例的过程。启用lean后Mongoose 直接返回纯 JavaScript 对象Plain Old JavaScript ObjectsPOJO不再是 Mongoose 文档。默认情况下Mongoose 查询返回的是 MongooseDocument类的实例。Document比原生 JavaScript 对象重得多因为它携带了大量用于变更追踪change tracking的内部状态。启用lean选项后Mongoose 跳过实例化完整 Mongoose 文档的步骤直接交出 POJOconst leanDoc await MyModel.findOne().lean();内存占用对比lean 文档可以小约 3 倍下面这段代码与仓库中的 test/docs/lean.test.js 中compare sizes lean vs not 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注意两点内存差异只影响 Node.js 进程内部占用不影响通过网络传输的数据量——因为JSON.stringify()的结果两者完全一致该测试在 Deno 环境下会被跳过见 test/docs/lean.test.js因为 Deno 不支持v8.serialize()说明这种对比依赖 Node.js 的 v8 序列化能力。类型对比DocumentvsObject从源码结构看查询执行后 Mongoose 会把结果从 POJO 转换为 Mongoose 文档而开启lean后这一步被跳过详见下文「源码视角」一节。类型上的直接体现是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; // Objectlean 文档缺失的能力启用lean的代价是 lean 文档不再拥有以下特性变更追踪Change tracking类型转换与校验Casting and validationGetter 和 Setter虚拟字段Virtualssave()方法下面这段代码说明Person模型的自定义 getter 和 virtual 在启用lean后都不会执行// 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 doesnt run leanDoc.lastName; // sisko, custom getter doesnt runlean与populatelean 选项自动传播populate()可以和lean()一起使用。如果同时使用两者lean选项会传播到被填充populated的文档上。下面例子中顶层的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虚拟字段填充Virtual 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从源码可以印证这种「传播」机制在 lib/query.js 的Query.prototype.populate()实现中只要this._mongooseOptions.lean不为空就会把lean写入每个 populate 子选项的options.lean除非用户已显式指定。而在填充结果的处理阶段lib/helpers/populate/assignRawDocsToIdStructure.js 与 lib/helpers/populate/assignVals.js 通过leanPopulateMaplib/helpers/populate/leanPopulateMap.js记录每个 lean 文档对应的模型lib/model.js 在 populate 查询返回时执行leanPopulateMap.set(val, mod.model)从而保证被填充的子文档同样保持 POJO 形态而不被水合。何时该用leanRESTful 路由实战判断如果查询结果将被原样发送出去例如直接传给 Express 的 response就应该使用lean。总的来说如果你不修改查询结果且不依赖自定义 getter就应该用lean()如果你会修改查询结果或依赖 getter、toObject()的 transform 等特性就不应该用lean()。下面是一个适合lean()的 Express 路由 示例——该路由不修改person文档也不依赖任何 Mongoose 专有功能// As long as you dont need any of the Person models 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等会修改数据、需要save()的路由一般不应使用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 })); });另外务必牢记virtual 不会出现在lean()的查询结果中。如果确实需要在 lean 结果中带上 virtual可以借助 mongoose-lean-virtuals 插件见下一节。用插件找回被lean绕过的功能使用lean()会绕过所有 Mongoose 特性包括 virtuals、getter/setter 和 默认值defaults。如果希望在lean()下使用这些特性需要借助对应的插件mongoose-lean-virtuals为 lean 查询结果补充虚拟字段mongoose-lean-getters在 lean 结果中执行 gettermongoose-lean-defaults在 lean 结果中应用默认值但必须注意Mongoose 不会水合 lean 文档因此在这些插件的 virtual、getter 和 default 函数中this是一个 POJO而不是 Mongoose 文档。例如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. });也就是说this.name这种直接属性访问可以正常工作但this.get(name)这类文档方法会崩溃因为this上根本没有get()方法。BigInt 与useBigInt64默认情况下MongoDB 官方 Node 驱动会把 MongoDB 中存储的 long 类型转换为 JavaScript 的 number而不是 BigInt。如果希望在lean()查询中把 long 还原成 BigInt可以设置useBigInt64选项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要点梳理写入阶段Mongoose 会把 schema 中声明为BigInt的字段正确存储为 MongoDB long读取阶段不设置useBigInt64时lean()读回的age是 number这与普通文档一致读取阶段通过setOptions({ useBigInt64: true })显式开启后lean()读回的age是bigint。在 lib/query.js 的查询选项文档中useBigInt64被明确列为lean相关的可选配置项之一。源码视角lean在查询执行链路中如何工作为了更深入地理解lean的底层原理可以追踪 lib/query.js 中的完整链路1.lean()方法本身只写一个内部标记Query.prototype.lean()的实现非常轻量lib/query.jsQuery.prototype.lean function(v) { this._mongooseOptions.lean arguments.length ? v : true; return this; };不传参数时等价于lean(true)传false可以显式关闭参数实际上可以是布尔值或对象——传对象时支持{ transform: fn }见下文它写入的是内部字段_mongooseOptions.lean属于查询的「mongoose 选项」与发送给 MongoDB 的查询参数如 filter、projection相互独立。2. 查询执行时lean 决定走哪条结果处理分支在执行单文档查询时lib/query.jsMongoose 会先把_mongooseOptions.lean同步到发给驱动的options.lean然后分支处理启用lean且没有populate时走_completeOneLean(model.schema, doc, ...)跳过completeOne即跳过文档水合启用lean且有populate时先执行model.populate(doc, pop)再走_completeOneLean未启用lean时走completeOne(model, doc, ...)把 POJO 转换为完整 Mongoose 文档。3.lean.transform对象形式lean()的额外能力_completeOneLeanlib/query.js支持lean({ transform: fn })这种对象用法当opts.lean.transform是函数时会对顶层文档以及所有子 schemachildSchemas对应的嵌套文档逐一调用transform。_completeManyLeanlib/query.js则对批量查询如find()的结果数组做同样处理。这提供了一种在保持 POJO 的同时对结果做后处理的原生手段。4. schema 级lean让模型默认返回 POJO除了每次查询手动调用.lean()还可以在 schema 层面全局开启lib/query.jsconst schema new mongoose.Schema({ name: String }, { lean: true });从 lib/query.js 的实现看当options.lean null且 schema 的 options 中存在lean属性时Mongoose 会把它写入this._mongooseOptions.lean。这意味着如果项目里绝大多数查询都是只读的可以把lean设为 schema 默认值再在个别需要完整文档的查询上显式.lean(false)关闭。5. 与游标cursor配合的高性能只读场景在 lib/query.js 的lean()文档注释中明确指出lean 非常适合高性能、只读的场景尤其是与游标cursor结合使用见 查询文档中的流式读取。当批量遍历大量文档且无需修改时lean能避免为每个文档创建重量级Document实例减少 GC 压力。6. 测试佐证本仓库的 test/docs/lean.test.js 完整覆盖了上述全部场景内存对比、类型对比、getter/virtual 行为、常规 populate、virtual populate 以及 BigInt/useBigInt64与 docs/tutorials/lean.md 的每个代码块一一对应代码块上的acquit:Lean Tutorial.*...标记即表明该片段由测试自动抽取生成。此外 test/docs/defaults.test.js 展示了findOneAndUpdate与lean()配合、以及setDefaultsOnInsert相关行为的用例可作为扩展阅读。小结lean()是 Mongoose 中最简单也最有效的只读查询优化手段它通过跳过文档水合让结果保持 POJO 形态换来约 3 倍的内存收益和更快的查询速度。使用时的核心权衡是不修改、不依赖 Mongoose 专有特性getter、virtual、defaults、save()的查询就放心用lean()需要这些能力的场景要么不用要么借助 mongoose-lean-virtuals、mongoose-lean-getters、mongoose-lean-defaults 插件补齐并记得此时this是 POJO。同时lean选项会自动传播到populate()的结果中并可通过useBigInt64控制 BigInt 的还原配合 schema 级lean: true默认值与游标流式读取可以让只读链路在保持代码简洁的同时获得可观的性能提升。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考