วันนี้ได้ไป review code ของน้องๆ มา ไปเจอว่าน้องใช้ lean() ร่วมในการ query โดยหลักการทำงานของ lean() ใน Mongoose คือการบอกให้ Mongoose ส่งคืนผลลัพธ์การ Query เป็น Plain Old JavaScript Objects (POJOs) แทนที่จะเป็น Mongoose Documents แบบเต็มรูปแบบ ซึ่งเป็นค่าเริ่มต้น (default)

Mongoose Documents

โดยปกติแล้ว เมื่อเรา Query ข้อมูลจาก MongoDB ด้วย Mongoose โดยไม่ใช้ lean() Mongoose จะสร้าง Mongoose Document ขึ้นมา

Mongoose Document มันไม่ใช่แค่ JavaScript object ธรรมดา แต่มันเป็น instance ของคลาส mongoose.Document ที่มีคุณสมบัติ และ เมธอดพิเศษมากมาย เช่น:

  • Change Tracking: Mongoose จะติดตามการเปลี่ยนแปลงของข้อมูลใน Document นั้นๆ เพื่อให้เราสามารถเรียกใช้ .save() เพื่อบันทึกการเปลี่ยนแปลงกลับไปยังฐานข้อมูลได้
  • Getters/Setters: เราสามารถกำหนด logic พิเศษเพื่อเข้าถึง หรือ แก้ไข field บางอย่างได้
  • Virtuals: เราสามารถสร้าง field เสมือนที่ไม่ได้เก็บอยู่ในฐานข้อมูลจริง แต่คำนวณจาก field อื่นๆ ได้
  • Instance Methods: เราสามารถเพิ่มเมธอดที่ทำงานบน Document นั้นๆ ได้

ข้อเสียของ Mongoose Documents (ในบางกรณี)

ฟังแล้วมันก็ดีนิ แต่ในบางกรณีการสร้าง Mongoose Document ขึ้นมานั้น มันมี Overhead (ค่าใช้จ่าย) ด้านประสิทธิภาพ และ หน่วยความจำ

เพราะต้องมีการสร้าง instance ของคลาสแล้ว แนบคุณสมบัติ และ เมธอดต่างๆ เข้าไป

หากเราเพียงแค่ต้องการดึงข้อมูลมาแสดงผล หรือ ส่งต่อข้อมูลไปยังส่วนอื่น โดยไม่ได้มีการแก้ไข หรือ ใช้คุณสมบัติพิเศษของ Mongoose Document เลย การสร้าง Document แบบเต็มรูปแบบก็ถือเป็นสิ่งที่ไม่จำเป็นและสิ้นเปลือง


lean() เข้ามาแก้ปัญหานี้

เมื่อเราใช้ lean() ใน Query ของเรา (เช่น MyModel.find().lean()) Mongoose จะข้ามขั้นตอนการ Hydrating (เติมบางอย่างเข้าไป) หรือ การแปลงข้อมูลที่ได้จาก MongoDB ให้เป็น Mongoose Document

แทนที่จะเป็น Mongoose Document Mongoose จะส่งคืนข้อมูลเป็น JavaScript object ธรรมดาๆ (POJO - Plain Old JavaScript Object) ซึ่งมีน้ำหนักเบากว่า และ ใช้หน่วยความจำน้อยกว่ามาก

ผลลัพธ์ คือ Query จะทำงานได้เร็วขึ้น และ ใช้ทรัพยากรน้อยลงอย่างเห็นได้ชัด โดยเฉพาะอย่างยิ่งเมื่อมีการดึงข้อมูลจำนวนมาก

ข้อจำกัด / สิ่งที่ต้องพิจารณาเมื่อใช้ lean():

  1. ไม่สามารถใช้ .save() ได้: เนื่องจากผลลัพธ์เป็น POJO ไม่ใช่ Mongoose Document เราจะไม่สามารถเรียกใช้เมธอด .save() เพื่อบันทึกการเปลี่ยนแปลงกลับไปยังฐานข้อมูลได้
  2. ไม่รองรับ Getters/Setters: Getters และ Setters ที่เรากำหนดใน Schema จะไม่ทำงานกับ POJO
  3. ไม่รองรับ Virtuals โดยตรง: Virtuals จะไม่ปรากฏในผลลัพธ์ lean() โดยค่าเริ่มต้น หากเราต้องการใช้ Virtuals กับ lean() เราอาจจะต้องใช้ plugin เพิ่มเติม เช่น mongoose-lean-virtuals
  4. ไม่รองรับ Instance Methods: เมธอดที่เรากำหนดให้กับ Schema (เช่น schema.methods.myCustomMethod) จะไม่สามารถเรียกใช้บน POJO ได้

สรุปง่ายๆ คือ เราจะไม่สามารถใช้ความสามารถของ mongoose.Document ได้ ถ้าเราอยากจะใช้จะต้องลง plugin เสริมเข้าไปแทน

เมื่อไหร่ที่ควรใช้ lean()

ง่ายๆ เลย เมื่อเราต้องการปรับปรุงประสิทธิภาพการ Query สำหรับข้อมูลจำนวนมาก เมื่อนั้นก็คิดถึงมันได้เลย

เมื่อเราต้องการดึงข้อมูลมาแสดงผลใน API response (เช่น Express routes สำหรับ GET requests)

เมื่อเราต้องการดึงข้อมูลมาประมวลผลต่อใน logic ของเราโดยไม่จำเป็นต้องมีการบันทึกกลับไปยังฐานข้อมูล

const users = await User.find({});
console.log(users[0] instanceof mongoose.Document); // true

users[0].name = 'New Name';
await users[0].save(); // สามารถบันทึกการเปลี่ยนแปลงได้

ไม่ใช้ lean() (ค่าเริ่มต้น)

เมื่อใช้ lean()

const leanUsers = await User.find({}).lean();
console.log(leanUsers[0] instanceof mongoose.Document); // false

// leanUsers[0].name = 'Another Name';
// await leanUsers[0].save(); // จะเกิดข้อผิดพลาด เพราะไม่ใช่ Mongoose Document

ใช้ lean()


BigInts

ตามค่าเริ่มต้น MongoDB Node driver จะเปลี่ยน longs ที่เก็บใน MongoDB เป็นตัวเลข JavaScript ไม่ใช่ BigInts

ถ้าเราต้องการใช้ BigInts ให้กำหนดค่าตัวเลือก useBigInt64 ใน options ของ lean() เพื่อเปลี่ยน longs ให้เป็น BigInts

ตัวอย่าง

onst Person = mongoose.model('Person', new mongoose.Schema({
  name: String,
  age: BigInt
}));

const { age } = await Person.create({ name: 'Benjamin Sisko', age: 37 });
typeof age; // 'bigint'

Mongoose จะทำการ convert age เป็น BigInt

let person = await Person.findOne({ name: 'Benjamin Sisko' }).lean();
typeof person.age; // 'number'

แต่ตอนที่เรา query กลับมา โดยใช้ lean() ตัว age จะเป็นตัวแปรประเภท number

person = await Person.findOne({ name: 'Benjamin Sisko' }).
  setOptions({ useBigInt64: true }).
  lean();
typeof person.age; // 'bigint'

วิธีแก้ คือ ให้กำหนดค่า useBigInt64 เข้าไปใน setOptions()


สรุป คือ lean() เป็นเครื่องมือที่มีประโยชน์มากในการปรับปรุงประสิทธิภาพของ Mongoose queries เมื่อเราไม่ต้องการใช้คุณสมบัติพิเศษของ Mongoose Documents และ ต้องการเพียงแค่ข้อมูลที่เป็น JavaScript object ธรรมดาๆ เท่านั้น


References