Vấn đề: Hiệu năng và Chức năngĐây là một kỹ thuật tối ưu hóa hiệu năng phổ biến: bạn thêm .lean() vào truy vấn Mongoose để rút ngắn thời gian thực thi và giảm mức chiếm dụng bộ nhớ. Theo mặc định, các document của Mongoose rất nặng. Chúng chứa trạng thái nội bộ, logic kiểm tra (validation) và các hook theo dõi thay đổi, điều này có thể khiến chúng chiếm bộ nhớ gấp 10 lần so với một đối tượng JSON thông thường.
Rắc rối bắt đầu khi bạn cố gắng xử lý đối tượng nhẹ đó như một document Mongoose đầy đủ. Bạn thay đổi một thuộc tính và gọi doc.save(), để rồi chứng kiến ứng dụng của mình bị crash:
TypeError: doc.save is not a function
Điều này xảy ra vì .lean() yêu cầu Mongoose bỏ qua quá trình khởi tạo một instance Document Mongoose đầy đủ. Thay vào đó, nó trả về một Plain Old JavaScript Object (POJO). Những đối tượng này rất nhanh, nhưng chúng thiếu đi các "phép thuật" nội bộ của Mongoose, bao gồm .save(), .populate(), và .validate().
Quy trình DebugĐể xác nhận rằng .lean() là nguyên nhân gây ra vấn đề, hãy kiểm tra constructor của document trước khi gọi phương thức save. Một document Mongoose tiêu chuẩn bao gồm các thuộc tính nội bộ như $__ và isNew, những thứ này sẽ không tồn tại trên một đối tượng lean.
const user = await User.findOne({ email: 'dev@example.com' }).lean();
console.log(user instanceof mongoose.Document); // Trả về: false
console.log(user.constructor.name); // Trả về: Object (thay vì 'model')
user.name = 'Tên đã cập nhật';
await user.save(); // Điều này kích hoạt TypeError
Nếu kết quả hiển thị là một Object thông thường, chuỗi prototype đã bị mất. Bạn không thể sử dụng bất kỳ phương thức đặc thù nào của Mongoose trên đó.
Giải pháp 1: Khắc phục trực tiếp (Xóa .lean())Giải pháp đơn giản nhất là xóa .lean() nếu bạn có ý định sửa đổi document và lưu lại. Khi bạn bỏ qua .lean(), Mongoose sẽ trả về một document đầy đủ với tất cả các phương thức nguyên vẹn.
// TRƯỚC
const user = await User.findById(id).lean();
// SAU (Đã sửa)
const user = await User.findById(id);
user.status = 'active';
await user.save();
Hãy chọn cách tiếp cận này khi bạn cần kích hoạt middleware của Mongoose, chẳng hạn như các hook pre('save'), hoặc khi bạn phụ thuộc vào logic kiểm tra schema phức tạp.
Giải pháp 2: Sử dụng các phương thức cập nhật AtomicĐôi khi bạn muốn tốc độ của .lean() cho lần lấy dữ liệu ban đầu, hoặc có lẽ bạn chỉ cần thay đổi một giá trị duy nhất trong cơ sở dữ liệu. Trong những trường hợp này, hãy sử dụng findOneAndUpdate hoặc updateOne. Các phương thức này bỏ qua hoàn toàn instance document và làm việc trực tiếp với MongoDB.
const userId = '60d5ec...';
// 1. Lấy dữ liệu với lean để xử lý logic chỉ đọc nhanh hơn
const user = await User.findById(userId).lean();
// 2. Cập nhật trực tiếp thông qua model
await User.updateOne({ _id: userId }, { $set: { status: 'active' } });
Đây thường là con đường hiệu quả nhất. Nó tránh được gánh nặng CPU của vòng đời document Mongoose—validation, hook và theo dõi thay đổi—làm cho nó trở nên lý tưởng cho các hoạt động ghi có lưu lượng truy cập cao.
Giải pháp 3: Tái tạo đối tượng (Nâng cao)Thỉnh thoảng bạn có thể sở hữu một đối tượng thuần túy từ bộ nhớ đệm (như Redis) mà đột nhiên cần chuyển đổi nó ngược lại thành một document Mongoose. Đối với những trường hợp đặc biệt này, hãy sử dụng model.hydrate().
const leanDoc = await User.findOne({ name: 'Alice' }).lean();
// Chuyển đổi POJO ngược lại thành một Document Mongoose đầy đủ
const doc = User.hydrate(leanDoc);
doc.status = 'inactive';
await doc.save(); // Bây giờ mã này đã hoạt động!
Lưu ý rằng hydrate() tạo ra một document mà Mongoose giả định là đã tồn tại trong cơ sở dữ liệu. Nó sẽ không kích hoạt logic isNew trừ khi bạn thay đổi trạng thái đó theo cách thủ công.
Xác minh: Xác nhận cập nhậtSau khi áp dụng bản sửa lỗi, hãy đảm bảo dữ liệu thực sự đã được cập nhật vào collection MongoDB của bạn. Bạn có thể xác minh bằng một vài phương pháp sau:
- Kiểm tra đối tượng trả về từ
await doc.save(); nó phải chứa các trường đã được cập nhật.- Sử dụng GUI như MongoDB Compass hoặc Mongo shell để chạy:db.users.find({ _id: ... }).- Ghi log thông báo thành công ngay sau lệnh gọi để đảm bảo event loop không bị treo.## Các điểm lưu ý chính- Lean dùng cho việc đọc: Chỉ nên dành.lean()cho các API GET, xuất dữ liệu dashboard hoặc các template nặng về đọc dữ liệu, nơi hiệu năng là ưu tiên hàng đầu.- Prototype rất quan trọng: Các phương thức như.save()nằm trên prototype của lớp Document. Các đối tượng thuần túy (plain objects) không kế thừa chúng.- Chọn đúng công cụ: Nếu bạn chỉ cập nhật một trường,updateOnesẽ nhanh hơn và tốn ít bộ nhớ hơn mô hìnhfind -> modify -> save.

