Tăng Cường Mongoose: Cách Sử Dụng Getter, Setter và Tính Năng Khác
Giới thiệu
Trong nhiều năm qua, tôi đã xây dựng, triển khai và duy trì các ứng dụng Node.js, và Mongoose, thư viện ODM hàng đầu cho MongoDB, đã trở thành công cụ ưa thích của tôi. Với Mongoose, bạn có thể dễ dàng quản lý dữ liệu không quan hệ và thực hiện các thao tác phức tạp mà không gặp phải những khó khăn của các API gốc của MongoDB.
Mongoose không chỉ đơn giản là một công cụ để định nghĩa schema; nó còn cung cấp nhiều tính năng mạnh mẽ giúp cải thiện cấu trúc, xác thực và bảo trì mã nguồn của bạn. Trong bài viết này, chúng ta sẽ khám phá sâu hơn về các tính năng như getters, setters, virtuals, custom validators, middleware, statics và methods để bạn có thể viết mã sạch hơn và thông minh hơn.
Getters và Setters
Mỗi lập trình viên đều đã trải qua những tình huống yêu cầu dữ liệu được lưu trữ khác biệt với cách sử dụng trong mã. Một ví dụ phổ biến là lưu trữ tên và email ở dạng chữ thường để đảm bảo tính nhất quán, nhưng hiển thị chúng ở dạng chữ hoa đầu. Với getters và setters, bạn có thể xử lý điều này ngay tại cấp độ schema:
javascript
const transactionSchema = new mongoose.Schema({
email: {
type: String,
set: v => v.toLowerCase(),
},
firstName: {
type: String,
set: v => v.toLowerCase(),
get: v => toTitleCase(v), // giả sử bạn đã có hàm toTitleCase
},
lastName: {
type: String,
set: v => v.toLowerCase(),
get: v => toTitleCase(v),
},
});
Các phương thức get và set là các hàm cho phép bạn sửa đổi biến như mong muốn trước khi nó được lấy từ hoặc lưu vào cơ sở dữ liệu, tương tự như các transformer trong TypeORM.
Virtuals
Giả sử nhóm sản phẩm muốn hiển thị tên đầy đủ của người dùng trên giao diện. Thay vì kết hợp tên một cách thủ công trong mọi nơi trong mã, bạn có thể định nghĩa một thuộc tính ảo. Nó cho phép bạn định nghĩa các thuộc tính được tính toán mà không thực sự tồn tại trong cơ sở dữ liệu.
javascript
userSchema.virtual('fullName').get(function () {
return `${this.firstName} ${this.lastName}`;
});
Điều này cho phép trường fullName được trả về mỗi khi truy vấn bộ sưu tập người dùng, mặc dù nó không được lưu trong cơ sở dữ liệu.
Middleware (Hooks)
Hãy nghĩ về middleware như một logic thông thường mà bạn muốn thực hiện trước khi tiếp tục với logic chính, trong trường hợp này là sự kiện mô hình, như save, find, remove, và hơn thế nữa. Mongoose cung cấp middleware dựa trên sự kiện tích hợp. Nó có thể được sử dụng cho xác thực phức tạp, loại bỏ tài liệu phụ thuộc, kích hoạt và nhiều hơn nữa.
javascript
userSchema.pre('save', function (next) {
if (this.phoneNumber) {
this.phoneNumber = formatPhoneNumber(this.phoneNumber); // định dạng số điện thoại theo định dạng mong muốn
}
next();
});
userSchema.pre('remove', async function (next) {
await Post.deleteMany({ author: this._id }); // xóa tất cả bài viết của người dùng
next();
});
userSchema.pre(/^find/, function (next) {
this.where({ isDeleted: { $ne: true } }); // loại trừ người dùng đã xóa mềm
next();
});
Với cách này, chúng ta định dạng một số trường trước khi lưu, xóa dữ liệu phụ thuộc để tránh dữ liệu mồ côi, và loại trừ người dùng đã xóa mềm mà không cần lo lắng về việc thêm điều kiện đó mỗi khi thực hiện truy vấn.
Statics và Methods: Hành Vi Giống Như Lớp
Mongoose models hoạt động giống như các lớp, và cũng giống như trong các lớp, bạn có thể thêm các phương thức tĩnh (mức mô hình) và phương thức thể hiện (mức tài liệu):
javascript
userSchema.statics.findByEmail = function (email) {
return this.findOne({ email });
};
userSchema.methods.isAdult = function () {
return this.age >= 18;
};
Điều này có nghĩa là:
javascript
const user = await User.findByEmail('xyz@yopmail.com');
console.log(user.isAdult()); // true hoặc false
Sạch sẽ, biểu cảm và hướng đối tượng.
Các Tính Năng Khác
Một số tính năng schema bổ sung giúp mô hình của bạn tốt hơn:
- match:
/^[a-zA-Z0-9_-]$/- Điều này cho phép bạn áp dụng các ký tự hợp lệ cho một trường bằng cách sử dụng regex. - index: true - Tăng hiệu suất truy vấn khi bạn truy vấn theo trường đó. Hữu ích khi bạn có một trường thường xuyên được truy vấn. Bạn cũng có thể sử dụng chỉ mục phức hợp.
- unique: true - Ngăn chặn giá trị trùng lặp cho một trường. Nó cũng tạo một chỉ mục trên trường đó.
Tùy Chọn Cấp Độ Schema
- timestamps: true - Tự động thêm createdAt và updatedAt.
- expireAt:
{ type: Date, default: Date.now, expires: 600 }- Đây là một chỉ mục TTL (Time-to-live) tự động xóa tài liệu sau 10 phút (có thể cấu hình) sau khi trường expireAt được thiết lập hoặc sau khi tài liệu được tạo ra. - toObject và toJSON:
{ getters: true, virtuals: true }- Các tùy chọn schema này chuyển đổi tài liệu mongoose thành một đối tượng JavaScript và có thể chứa các sửa đổi áp dụng cho mỗi tài liệu trong mô hình.
Mô hình cuối cùng trông như thế này:
javascript
const userSchema = new mongoose.Schema({
email: {
type: String,
set: v => v.toLowerCase(),
unique: true,
},
firstName: {
type: String,
set: v => v.toLowerCase(),
get: v => toTitleCase(v), // giả sử bạn đã có hàm toTitleCase
},
lastName: {
type: String,
set: v => v.toLowerCase(),
get: v => toTitleCase(v),
},
username: {
type: String,
match: /^[a-zA-Z0-9_-]$/,
set: v => v.toLowerCase(),
unique: true,
},
age: {
type: Number,
},
pin: {
type: String,
},
phoneNumber: {
type: String,
},
},
{
timestamps: true,
toJSON: { getters: true, virtuals: true },
toObject: { getters: true, virtuals: true },
});
userSchema.virtual('fullName').get(function () {
return `${this.firstName} ${this.lastName}`;
});
userSchema.pre('save', function (next) {
if (this.phoneNumber) {
this.phoneNumber = formatPhoneNumber(this.phoneNumber); // định dạng số điện thoại theo định dạng mong muốn
}
next();
});
userSchema.pre('remove', async function (next) {
await Post.deleteMany({ author: this._id }); // xóa tất cả bài viết của người dùng
next();
});
userSchema.pre(/^find/, function (next) {
this.where({ isDeleted: { $ne: true } }); // loại trừ người dùng đã xóa mềm
next();
});
userSchema.statics.findByEmail = function (email) {
return this.findOne({ email });
};
userSchema.methods.isAdult = function () {
return this.age >= 18;
};
export const User = mongoose.model('User', userSchema);
Kết luận
Như vậy, bạn đã thấy rằng Mongoose không chỉ đơn giản là một công cụ xác thực bổ sung hay một lớp bảo vệ; nó thực sự là một thư viện mạnh mẽ giúp bạn tối ưu hóa mã nguồn của mình. Hãy tận dụng tất cả các tính năng mà Mongoose cung cấp để viết mã sạch, hiệu quả và dễ bảo trì hơn. Nếu bạn chưa áp dụng những kỹ thuật này vào dự án của mình, hãy bắt đầu ngay hôm nay để thấy sự khác biệt!
Câu hỏi thường gặp (FAQ)
- Mongoose có thể sử dụng với loại cơ sở dữ liệu nào?
Mongoose được thiết kế dành riêng cho MongoDB, do đó nó không thể sử dụng với các loại cơ sở dữ liệu khác. - Có cách nào để tối ưu hóa hiệu suất truy vấn không?
Có, bạn có thể sử dụng các tùy chọn nhưindex,uniquevà các phương thức tĩnh để cải thiện hiệu suất. - Tôi có thể sử dụng Mongoose với TypeScript không?
Có, Mongoose hỗ trợ TypeScript và bạn có thể định nghĩa các kiểu cho schema của mình.
Hãy bắt đầu áp dụng ngay những kỹ thuật này vào dự án của bạn để tối ưu hóa mã nguồn và nâng cao hiệu suất làm việc!