使用 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 文件沒有:

  • 變更追蹤
  • 轉換和驗證
  • getter 和 setter
  • 虛擬屬性
  • save()

例如,以下程式碼範例顯示,如果您啟用 leanPerson 模型的 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 和 Populate

Populatelean() 搭配使用。如果您同時使用 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()。一般來說,在 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 查詢結果中。

外掛(Plugins)

使用 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 中的 long 轉換為 JavaScript 數字,而不是 BigInts。在您的 lean() 查詢上設定 useBigInt64 選項,將 long 擴充為 BigInts。

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'