Kết nối MongoDB với Node.js bằng Mongoose
Xem thửTình huống mở đầu
Chú Tư, người dựng trang web cho Tạp Hóa Cô Tấm ở Phố Rôm Rả, được giao một việc: cho khách tạo tài khoản và lưu vào database shop. Anh viết sáu dòng Node.js, bấm chạy, và mọi thứ có vẻ ổn. Sáng hôm sau anh quên bật mongod, mở trang lên, và nó đứng im. Không lỗi, không chữ nào. Đúng 10 giây sau mới có một thông báo buffering timed out.
Anh gãi đầu: sao không báo lỗi ngay, và 10 giây đó ứng dụng đã làm gì? Anh còn một thắc mắc khác: trong schema anh viết unique: true cho email, vậy là Mongoose sẽ từ chối email trùng đúng không?
Ôn nhanh bài trước: mongorestore mặc định gặp document có khóa duy nhất trùng với document đã có thì làm gì?
Đáp án: bỏ qua document đó, không ghi đè và không dừng. Bài này mở chương mới: đưa ứng dụng Node.js vào nói chuyện với MongoDB bằng Mongoose, với Node.js v22.21.1, Mongoose 9.11.4 và MongoDB 4.4.19.
Mục tiêu bài học
Sau bài này bạn có thể:
- Nối ứng dụng Node.js với MongoDB bằng
mongoose.connect, lấyURItừ biến môi trường, và bắt được lỗi khi không nối được. - Khai báo schema và model, đoán đúng tên collection mà Mongoose tạo ra, và đọc được
ValidationErrortừrequired,minlength,match,min. - Giải thích vì sao
uniquekhông phải là validator mà là index, và nhận ra lỗi trùng khóa bằng mã 11000. - Đoán được điều xảy ra khi gọi lệnh trước khi nối xong, và đóng kết nối đúng để tiến trình tự thoát.
Bức tranh lớn
Xuyên suốt bài, Mongoose là cô lễ tân đứng giữa trang web và cuốn sổ của tiệm. Khách đưa phiếu, lễ tân kiểm tra phiếu có đủ mục, đúng dạng không rồi mới chuyển vào sổ. Nhưng chuyện hai người trùng tên đăng ký là do người giữ sổ, tức mongod, từ chối. Dự án xuyên suốt của chương là cho trang của Tạp Hóa Cô Tấm có tài khoản khách: bài này dựng đường nối, hai bài sau làm đăng ký và đăng nhập, bài cuối thử với Django.
Hình dưới trả lời một câu hỏi: một lệnh từ ứng dụng đi qua những chặng nào trước khi tới database?
Đang tải sơ đồ…
Cách đọc hình: route của ứng dụng (ô cyan) gọi model (ô tím). Model hỏi kết nối đã sẵn sàng chưa (ô cam). Rồi thì driver gửi lệnh đến mongod (ô xanh dương) và dữ liệu nằm trong shop.users (ô xanh lá). Chưa thì lệnh vào hàng chờ (ô tím) tối đa 10 giây.
Chỗ ẩn dụ không còn đúng: cô lễ tân thật đứng yên một chỗ, còn Mongoose là thư viện chạy ngay trong tiến trình Node của bạn, cùng bộ nhớ với code của bạn.
Khái niệm cốt lõi
Bài này có năm khái niệm.
1. Kết nối là một Promise
mongoose.connect(uri) trả về Promise. Gọi xong chưa có nghĩa là đã nối: mongoose.connection.readyState là 0 khi chưa nối, 2 khi đang nối, 1 khi đã nối. URI nên nằm trong biến môi trường (ở đây là tệp .env nạp bằng node --env-file=.env), vì URI thật chứa mật khẩu và không được nằm trong mã nguồn. Các tùy chọn cũ useNewUrlParser và useUnifiedTopology không còn cần nữa; ở bản này truyền vào còn gây lỗi.
2. Schema và model
Schema mô tả hình dạng một document: kiểu của từng field và các ràng buộc. Model là cái tay cầm để đọc ghi collection. mongoose.model("User", schema) tạo collection tên users: tên model viết thường và thêm s. Field không có trong schema bị Mongoose bỏ im lặng khi lưu (chế độ strict mặc định), nên khách không nhét được role: "admin" vào document.
3. Validation chạy trong ứng dụng
required, minlength, match, min và các setter như trim, lowercase đều do Mongoose chạy trong tiến trình Node, trước khi gửi gì cho máy chủ. Lỗi gom thành một ValidationError với từng field trong errors. Nhờ vậy validate() chạy được cả khi chưa nối database.
4. unique là index, không phải validator
unique: true chỉ là lời nhắc Mongoose tạo index duy nhất (bài index ở chương 52 đã dạy). Việc từ chối bản ghi trùng là của máy chủ, nên lỗi là MongoServerError mã 11000, không phải ValidationError. Nếu index chưa được tạo thì không có gì chặn trùng.
5. Hàng chờ và đóng kết nối
Mặc định Mongoose xếp lệnh gọi trước khi nối vào hàng chờ (bufferCommands) và bỏ cuộc sau 10 giây (bufferTimeoutMS). Kết nối đang mở giữ cho tiến trình Node không thoát: script xong việc mà quên mongoose.disconnect() thì cứ treo.
Ví dụ đầu tiên
Mọi lệnh chạy trong bash, trong một thư mục tạm tự xóa. Bước đầu là cài Mongoose. Bài này ghim đúng phiên bản 9.11.4; máy bạn có thể cài bản khác và hành vi chi tiết có thể khác:
node -v
npm -v
mkdir app && cd app
npm init -y > /dev/null
npm install mongoose@9.11.4 > /dev/null 2>&1
npm ls --depth=0 | tail -n +2 | grep .
npm ls mongodb | tail -n +2 | grep .
v22.21.1
10.9.4
└── mongoose@9.11.4
└─┬ mongoose@9.11.4
└── mongodb@7.7.0
Kết quả [Đã chạy: Node v22.21.1, npm 10.9.4, Mongoose 9.11.4 kéo theo driver mongodb 7.7.0]: khối trên. Driver 7.7.0 vẫn nói chuyện được với mongod 4.4.19 ở bài này.
Tệp .env chứa URI, và tệp db.js gói việc nối lại một chỗ:
const mongoose = require("mongoose");
async function connect() {
// Chờ tối đa 3 giây để tìm máy chủ, mặc định lâu hơn nhiều
await mongoose.connect(process.env.MONGODB_URI, { serverSelectionTimeoutMS: 3000 });
return mongoose.connection;
}
module.exports = { connect };
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
process.env.MONGODB_URI | Đọc URI từ biến môi trường | URI thật có mật khẩu, không dán vào mã nguồn |
serverSelectionTimeoutMS: 3000 | Chờ tối đa 3 giây để tìm thấy máy chủ | Mặc định là 30 giây theo tài liệu [Suy luận], quá lâu khi cấu hình sai |
await mongoose.connect(...) | Chờ Promise nối xong rồi mới đi tiếp | connect không chặn, await mới chặn |
return mongoose.connection | Trả về đối tượng kết nối | Để nơi gọi đọc readyState và name |
Tệp connect.js gọi hàm đó và in trạng thái qua từng chặng:
const mongoose = require("mongoose");
const { connect } = require("./db");
(async () => {
console.log("before connect:", mongoose.connection.readyState);
// connect trả Promise ngay, chưa chờ nối xong
const pending = connect();
console.log("right after the call:", mongoose.connection.readyState);
const conn = await pending;
console.log("after await:", conn.readyState, conn.name);
await mongoose.disconnect();
console.log("after disconnect:", mongoose.connection.readyState);
})();
printf 'MONGODB_URI=mongodb://127.0.0.1:27017/shop\n' > .env
node --env-file=.env connect.js
before connect: 0
right after the call: 2
after await: 1 shop
after disconnect: 0
Dự đoán trước khi xem kết quả: ngay sau khi gọi connect() mà chưa await, readyState là mấy?
Kết quả [Đã chạy: Node v22.21.1, Mongoose 9.11.4, MongoDB 4.4.19]: khối trên. Tên database in ra là shop, lấy từ cuối URI.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
readyState trước khi gọi | In 0 | Chưa có kết nối nào |
const pending = connect() | Bắt đầu nối, trả Promise ngay | Chưa await nên chưa nối xong |
readyState ngay sau | In 2 | Trạng thái đang nối |
await pending | Chờ nối xong, trạng thái 1 | Từ đây mới gửi lệnh được |
mongoose.disconnect() | Đóng kết nối, trạng thái về 0 | Cho tiến trình được phép thoát |
Giờ thêm schema. Tệp user.js khai báo model với bốn loại ràng buộc:
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
// unique chỉ nhắc Mongoose tạo index, không phải validator
email: { type: String, required: true, unique: true, trim: true, lowercase: true, match: /^[^@\s]+@[^@\s]+\.[^@\s]+$/ },
name: { type: String, required: [true, "name is required"], minlength: 2 },
age: { type: Number, min: 13 },
}, { timestamps: true });
module.exports = mongoose.model("User", userSchema);
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
required: true | Từ chối document thiếu field | Email là bắt buộc |
trim: true, lowercase: true | Cắt khoảng trắng và đổi chữ thường trước khi lưu | An@Shop.com và an@shop.com là một người |
match: /.../ | Kiểm tra email có dạng a@b.c | Chỉ là kiểm tra dạng, không chứng minh email có thật |
required: [true, "name is required"] | Bắt buộc, kèm thông báo lỗi tự chọn | Dạng mảng cho phép đặt chữ báo lỗi |
minlength: 2, min: 13 | Chặn tên quá ngắn và tuổi dưới 13 | Ràng buộc theo kiểu của field |
{ timestamps: true } | Tự thêm createdAt và updatedAt | Khỏi tự ghi ngày |
mongoose.model("User", userSchema) | Tạo model, collection tên users | Quy tắc đặt tên: thường hóa và thêm s |
Rồi thử các document hỏng bằng validate(). Chú ý lệnh này không có connect() nào:
const mongoose = require("mongoose");
const User = require("./user");
(async () => {
// validate chạy ngay trong ứng dụng, chưa cần nối database
const bad = new User({ email: "not-an-email", name: "A", age: 9 });
try {
await bad.validate();
} catch (err) {
console.log(err.name, "on:", Object.keys(err.errors).join(", "));
for (const [path, e] of Object.entries(err.errors)) console.log(" ", path, e.kind);
}
try {
await new User({ email: "an@shop.com", name: "An", age: "abc" }).validate();
} catch (err) {
console.log("age abc:", err.errors.age.name, "-", err.errors.age.message);
}
const clean = new User({ email: " An@Shop.COM ", name: "An", role: "admin" });
console.log("after setters:", JSON.stringify(clean.email), "| role:", clean.role);
console.log("collection:", User.collection.name);
console.log("readyState the whole time:", mongoose.connection.readyState);
})();
node validate.js
ValidationError on: email, name, age
email regexp
name minlength
age min
age abc: CastError - Cast to Number failed for value "abc" (type string) at path "age" for model "User"
after setters: "an@shop.com" | role: undefined
collection: users
readyState the whole time: 0
Kết quả [Đã chạy: Node v22.21.1, Mongoose 9.11.4]: khối trên. readyState là 0 từ đầu đến cuối mà validate() vẫn trả lời, vì nó chạy hết trong ứng dụng.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
bad.validate() | Ném ValidationError có ba field lỗi | Gom hết lỗi một lượt, không dừng ở lỗi đầu |
e.kind | In regexp, minlength, min | Mỗi ràng buộc có tên riêng để ứng dụng phân loại |
age: "abc" | Ra CastError cho field age | Không ép được chuỗi thành số nên là lỗi kiểu |
role: "admin" | clean.role là undefined | Field ngoài schema bị bỏ khi dựng document |
" An@Shop.COM " | Thành an@shop.com | Setter trim và lowercase đã chạy |
User.collection.name | In users | Xác nhận quy tắc đặt tên collection |
Cuối cùng là nối thật và thử trùng email:
const mongoose = require("mongoose");
const { connect } = require("./db");
const User = require("./user");
(async () => {
await connect();
// Chờ Mongoose dựng xong index trước khi ghi
await User.init();
console.log("indexes:", Object.keys(await User.collection.getIndexes()).join(", "));
await User.create({ email: "an@shop.com", name: "An" });
try {
await User.create({ email: "AN@shop.com", name: "Anh" });
} catch (err) {
console.log("second create:", err.name, err.code, JSON.stringify(err.keyValue));
console.log("is a validation error:", err instanceof mongoose.Error.ValidationError);
}
console.log("users stored:", await User.countDocuments());
await mongoose.disconnect();
})();
printf 'MONGODB_URI=mongodb://127.0.0.1:27017/shop\n' > .env
node --env-file=.env unique.js
mongo --quiet shop --eval 'print("collections:", db.getCollectionNames().join(", ")); print(JSON.stringify(db.users.getIndexes().map(function (i) { return i.name + ":" + (i.unique === true) })))'
indexes: _id_, email_1
second create: MongoServerError 11000 {"email":"an@shop.com"}
is a validation error: false
users stored: 1
collections: users
["_id_:false","email_1:true"]
Kết quả [Đã chạy: Node v22.21.1, Mongoose 9.11.4, MongoDB 4.4.19]: khối trên. Dòng collections và danh sách index lấy từ mongo shell 4.4.19 để đối chiếu với những gì Mongoose đã tạo.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
await User.init() | Chờ Mongoose dựng xong index | Không có dòng này, lệnh ghi đầu tiên có thể chạy trước khi index sẵn sàng |
getIndexes() | Thấy _id_ và email_1 | unique: true sinh ra index email_1 |
create({ email: "AN@shop.com" }) | Lỗi mã 11000 | Setter đã đổi thành an@shop.com nên trùng với bản đầu |
err.keyValue | {"email":"an@shop.com"} | Cho biết giá trị nào gây trùng |
instanceof ValidationError | false | Đây là lỗi của máy chủ, không phải của schema |
Xem từng bước
Animation dưới đây chạy năm việc theo thứ tự. Hãy trả lời câu dự đoán trước khi bấm bước đầu tiên.
Đang tải Animation…
Bảng trace bằng chữ [Đã chạy: Mongoose 9.11.4, MongoDB 4.4.19]:
| Lần | Việc làm | Kết quả |
|---|---|---|
| 1 | connect() rồi await | readyState đi từ 2 sang 1 |
| 2 | validate() document sai ba field | ValidationError, máy chủ chưa nhận gì |
| 3 | create user hợp lệ | Lưu được, 1 user |
| 4 | create lại AN@shop.com | MongoServerError mã 11000 |
| 5 | find() khi chưa nối | Chờ 10 giây rồi báo lỗi buffering |
Bạn vừa làm được: nối Node.js với MongoDB, khai báo schema có ràng buộc, và phân biệt lỗi do Mongoose với lỗi do máy chủ.
Concept Check
Dùng lại model User ở trên.
Câu 1. Chưa gọi connect() lần nào, new User({ email: "x", name: "A" }).validate() ra gì?
- A. Treo 10 giây rồi báo lỗi buffering.
- B.
ValidationError, vì validation chạy trong ứng dụng. - C.
MongoNotConnectedError, vì chưa có kết nối. - D. Trả về bình thường vì chưa kiểm tra gì.
Câu 2. Hai lần create với an@shop.com rồi AN@shop.com. Lần thứ hai lỗi gì?
- A.
ValidationErrorở fieldemail, vìuniquelà validator. - B. Không lỗi, vì chữ hoa và chữ thường là hai giá trị khác nhau.
- C.
CastError, vì email đã tồn tại. - D.
MongoServerErrormã 11000, do indexemail_1từ chối.
Câu 3. Vì sao kết quả tìm được User.collection.name là users chứ không phải User?
- A. Mongoose thường hóa tên model và thêm
s. - B. Vì trong
.envcó chữusers. - C. Vì driver tự đoán từ dữ liệu có sẵn.
- D. Vì
timestamps: trueđổi tên.
Đáp án và giải thích:
- Câu 1: B. Xem bảng validate ở trên:
readyStatelà 0 mà vẫn raValidationError. A và C chỉ đúng với lệnh chạm tới database. - Câu 2: D.
uniquechỉ tạo index, máy chủ mới từ chối; setterlowercasecòn làm hai giá trị thành một. - Câu 3: A. Đây là quy tắc đặt tên mặc định của Mongoose; đổi bằng đối số thứ ba của
mongoose.model.
Làm cùng mình
Chú Tư dặn: viết một script nối vào shop, in trạng thái kết nối và tên database, rồi đóng kết nối sạch sẽ.
Bước 1. Vẽ ra giấy
Theo hình Bức tranh lớn, viết ba việc cần làm theo thứ tự: nối, in trạng thái, đóng.
Bước 2. Điền chỗ trống
const mongoose = require("mongoose");
(async () => {
// TODO 1: nối bằng mongoose.connect với URI lấy từ biến môi trường MONGODB_URI
// TODO 2: in readyState và tên database của mongoose.connection
// TODO 3: đóng kết nối rồi in lại readyState
})();
Gợi ý chỗ trống thứ hai: mongoose.connection.name là tên database lấy từ URI.
Bước 3. So kết quả
Chạy bằng node --env-file=.env mine.js, rồi đối chiếu mục Lời giải và đáp án.
Bước 4. Hỏi thêm
Nếu bỏ dòng đóng kết nối ở chỗ trống thứ ba, script có thoát được không, và vì sao?
Thử phá nó đi
Năm thứ có thể hỏng: sai địa chỉ, tùy chọn cũ, lệnh gọi quá sớm, index chưa được tạo, và quên đóng kết nối. Hình dưới trả lời một câu hỏi: một lần create có thể bị từ chối ở những chặng nào?
Đang tải sơ đồ…
Cách đọc hình: lệnh create (ô cyan) đi qua hai cửa kiểm tra (ô cam). Cửa đầu là schema trong ứng dụng: sai thì dừng ngay với ValidationError và không gửi gì (ô đỏ). Qua được thì lệnh đến máy chủ, và cửa thứ hai là index duy nhất: trùng thì MongoServerError mã 11000 (ô đỏ), không trùng thì lưu được (ô xanh lá).
Sai địa chỉ và tùy chọn cũ
const mongoose = require("mongoose");
(async () => {
try {
await mongoose.connect("mongodb://127.0.0.1:27999/shop", { serverSelectionTimeoutMS: 1500 });
} catch (err) {
console.log("wrong port:", err.name, "-", err.message);
}
try {
// Hai tùy chọn của bản cũ, driver mới từ chối hẳn
await mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
} catch (err) {
console.log("old options:", err.name, "-", err.message);
}
})();
printf 'MONGODB_URI=mongodb://127.0.0.1:27017/shop\n' > .env
node --env-file=.env fail.js
wrong port: MongooseServerSelectionError - connect ECONNREFUSED 127.0.0.1:27999
old options: MongoParseError - options usenewurlparser, useunifiedtopology are not supported
Kết quả [Đã chạy: Mongoose 9.11.4, MongoDB 4.4.19; chữ báo lỗi có thể khác ở bản khác]: khối trên.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
127.0.0.1:27999 | Không có máy chủ nào nghe cổng này | Cổng được chọn là cổng trống để thử lỗi |
serverSelectionTimeoutMS: 1500 | Bỏ cuộc sau 1,5 giây | Muốn thấy lỗi nhanh thay vì chờ 30 giây |
MongooseServerSelectionError | Có ECONNREFUSED trong thông báo | Phải bọc connect trong try/catch hoặc .catch |
useNewUrlParser, useUnifiedTopology | MongoParseError: không hỗ trợ | Hai tùy chọn của bản cũ, driver mới từ chối hẳn thay vì lờ đi |
Bài cũ chỉ nói hai tùy chọn đó "không còn cần". Ở Mongoose 9.11.4 thì còn tệ hơn: truyền vào là lỗi, nên đoạn code copy từ bài hướng dẫn cũ sẽ dừng ngay lúc nối.
Gọi lệnh trước khi nối xong
const mongoose = require("mongoose");
const { connect } = require("./db");
const User = mongoose.model("User", new mongoose.Schema({ email: String }));
(async () => {
const started = Date.now();
try {
// Chưa nối nên lệnh nằm trong hàng chờ, 10 giây sau mới lỗi
await User.find();
} catch (err) {
console.log("not connected:", err.message);
console.log("waited about", Math.round((Date.now() - started) / 1000), "seconds");
}
const early = User.countDocuments({});
await connect();
console.log("query sent before connect, answered later:", await early);
await mongoose.disconnect();
mongoose.set("bufferCommands", false);
try {
await User.find();
} catch (err) {
console.log("bufferCommands false:", err.name, "-", err.message);
}
})();
printf 'MONGODB_URI=mongodb://127.0.0.1:27017/shop\n' > .env
node --env-file=.env buffer.js
not connected: Operation `users.find()` buffering timed out after 10000ms
waited about 10 seconds
query sent before connect, answered later: 0
bufferCommands false: MongoNotConnectedError - Client must be connected before running operations
Kết quả [Đã chạy: Mongoose 9.11.4; con số 10 giây là cấu hình mặc định, thời gian đo thật [Có thể khác nhau] vài chục mili giây]: khối trên.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
User.find() khi chưa nối | Chờ trong hàng chờ, rồi lỗi sau 10 giây | bufferTimeoutMS mặc định là 10000 |
User.countDocuments({}) rồi mới connect() | Lệnh chờ, rồi được trả lời khi nối xong | Hàng chờ giúp thứ tự khởi động không quá chặt |
bufferCommands là false | Lỗi ngay lập tức | Nhà vận hành thích biết lỗi ngay thay vì chờ |
Hàng chờ nghe như tính năng tử tế, nhưng nó che giấu lỗi cấu hình: ứng dụng cứ treo 10 giây rồi mới kêu. Cách an toàn hơn là await connect() xong rồi mới nhận request.
unique khi index không được tạo
const mongoose = require("mongoose");
const { connect } = require("./db");
// autoIndex tắt nên unique: true không sinh ra index nào
const schema = new mongoose.Schema({ email: { type: String, unique: true } }, { autoIndex: false });
const Loose = mongoose.model("Loose", schema, "loose_users");
const names = async () => Object.keys(await Loose.collection.getIndexes()).join(", ");
(async () => {
await connect();
await Loose.create({ email: "an@shop.com" });
await Loose.create({ email: "an@shop.com" });
console.log("saved twice:", await Loose.countDocuments(), "- indexes:", await names());
try {
await Loose.syncIndexes();
} catch (err) {
console.log("syncIndexes:", err.code, err.codeName);
}
await Loose.deleteOne({ email: "an@shop.com" });
await Loose.createIndexes();
console.log("after cleanup:", await Loose.countDocuments(), "- indexes:", await names());
try {
await Loose.create({ email: "an@shop.com" });
} catch (err) {
console.log("third create:", err.code);
}
await mongoose.disconnect();
})();
printf 'MONGODB_URI=mongodb://127.0.0.1:27017/shop\n' > .env
node --env-file=.env loose.js
saved twice: 2 - indexes: _id_
syncIndexes: 11000 DuplicateKey
after cleanup: 1 - indexes: _id_, email_1
third create: 11000
Kết quả [Đã chạy: Mongoose 9.11.4, MongoDB 4.4.19]: khối trên.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
{ autoIndex: false } | Mongoose không tự tạo index | Nhiều nhóm tắt autoIndex ở production |
Hai lần create cùng email | Cả hai được lưu | Không có index thì unique: true không chặn gì |
syncIndexes() | Lỗi 11000 DuplicateKey | Không dựng được index duy nhất khi dữ liệu đã trùng |
deleteOne rồi createIndexes() | Index email_1 xuất hiện | Dọn trùng trước rồi mới dựng index |
create lần thứ ba | Mã 11000 | Từ đây máy chủ chặn trùng |
Quên đóng kết nối
const mongoose = require("mongoose");
const { connect } = require("./db");
(async () => {
await connect();
console.log("work done");
// Quên dòng này thì kết nối mở giữ tiến trình sống mãi
if (process.argv[2] === "close") await mongoose.disconnect();
})();
printf 'MONGODB_URI=mongodb://127.0.0.1:27017/shop\n' > .env
timeout 4 node --env-file=.env exit.js; echo "forgot to close, exit code: $?"
node --env-file=.env exit.js close; echo "closed, exit code: $?"
work done
forgot to close, exit code: 124
work done
closed, exit code: 0
Kết quả [Đã chạy: Node v22.21.1; timeout 4 tự dừng tiến trình sau 4 giây]: khối trên.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
timeout 4 node exit.js | Mã thoát 124 | 124 là mã của timeout khi phải ngắt tiến trình đang treo |
exit.js close | Mã thoát 0 | disconnect() trả lại quyền thoát cho tiến trình |
Tự tay làm
Mỗi bài có đầu vào, kết quả mong đợi và tiêu chí đạt.
Cơ bản
Bài 1. Schema cho sổ hàng. Đầu vào: viết model Product có name bắt buộc dài ít nhất 2 ký tự, price bắt buộc không âm, sku bắt buộc và duy nhất. Dùng validate() thử { name: "rice", price: -5 }. Kết quả mong đợi: ValidationError ở price và sku. Tiêu chí đạt: một document hợp lệ đi qua. Gợi ý: min: 0 cho giá.
Vận dụng
Bài 2. Hết kiên nhẫn sớm. Đầu vào: một script chưa gọi connect(). Đặt thời gian chờ hàng chờ còn 1,5 giây rồi find(). Tiêu chí đạt: thông báo nói 1500ms và lệnh lỗi trong dưới 3 giây. Gợi ý: mongoose.set("bufferTimeoutMS", 1500).
Thử thách (tùy chọn)
Bài 3. Dọn trùng rồi dựng index. Đầu vào: collection loose_users có hai document an@shop.com và một binh@shop.com, schema tắt autoIndex. Viết script tìm nhóm trùng bằng aggregation, xóa bản thừa, dựng index, rồi chứng minh lần tạo trùng tiếp theo bị chặn. Tiêu chí đạt: còn 2 document và lỗi mã 11000. Gợi ý: $group theo email rồi $match nhóm có n lớn hơn 1.
Tự kiểm tra
Trả lời bằng lời của bạn, không nhìn lại bài.
- (Mục tiêu 1, nhớ lại) Ngay sau khi gọi
mongoose.connect()mà chưaawait,readyStatebằng mấy? - (Mục tiêu 1, giải thích) Vì sao
URIđể trong.envthay vì trong mã nguồn? - (Mục tiêu 2, dự đoán)
mongoose.model("Product", schema)tạo collection tên gì? - (Mục tiêu 3, tìm lỗi) Một đồng nghiệp bảo "tôi đặt
unique: truerồi nên không cần bắt lỗi trùng". Nói sai ở đâu? - (Mục tiêu 4, chọn cách) Vì sao nên
await connect()rồi mới lắng nghe request thay vì dựa vào hàng chờ?
Điều cần nhớ
mongoose.connecttrả Promise: gọi xong chưa phải nối xong;readyState0, 2, 1 là chưa nối, đang nối, đã nối.URInằm trong biến môi trường; hai tùy chọnuseNewUrlParservàuseUnifiedTopologyở Mongoose 9.11.4 gâyMongoParseError.- Validation và setter chạy trong ứng dụng, gom thành
ValidationError;validate()chạy được cả khi chưa nối. unique: truelà index: lỗi làMongoServerErrormã 11000; tắtautoIndexhoặc dữ liệu đã trùng thì không có gì chặn.- Lệnh gọi trước khi nối chờ 10 giây rồi lỗi; quên
disconnect()thì tiến trình không thoát.
Cheat sheet:
| Muốn | Viết | Ghi chú |
|---|---|---|
| Nối | await mongoose.connect(process.env.MONGODB_URI) | Bọc try/catch |
| Nạp biến môi trường | node --env-file=.env app.js | Có từ Node 20.6 theo tài liệu Node [Suy luận] |
| Xem trạng thái | mongoose.connection.readyState | 0, 1, 2 như trên |
| Kiểm tra không cần nối | await doc.validate() | Ném ValidationError |
| Chờ index sẵn sàng | await Model.init() | Trước lệnh ghi đầu tiên |
| Nhận ra trùng khóa | err.code === 11000 | Không phải ValidationError |
| Chờ ngắn hơn | mongoose.set("bufferTimeoutMS", 1500) | Mặc định 10000 |
| Đóng | await mongoose.disconnect() | Để tiến trình thoát |
Hình tóm tắt của bài là sơ đồ ở mục Bức tranh lớn.
Câu hỏi để dành cho ngày mai: bây giờ trang web đã nối được và biết schema. Khi khách điền form đăng ký, mật khẩu của họ nên đi vào database ở dạng nào, và unique nên chặn trùng ở chỗ nào của luồng đó?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. Mongoose bọc driver chính thức
mongodb: nó gom cấu hình, kiểm tra schema, đổi giá trị theo kiểu rồi mới gọi driver. Hàng chờ chỉ là một danh sách lệnh trong bộ nhớ, được xả khi kết nối lên [Suy luận: theo tài liệu Mongoose, bài không đọc mã nguồn]. - So với lựa chọn khác. Dùng thẳng driver
mongodbcho ít lớp hơn nhưng bạn tự viết kiểm tra từng field; schema ở tầng database của chương 51 và 52 cho phép kiểm tra ngay ở máy chủ, cái mà Mongoose không thay được. - Hiểu lầm nâng cao. Bài cũ viết
import mongoose from "mongoose"vàawaitở cấp cao nhất mà không nói cần ESM. Bài cũ cũng liệt kêuniquenhư một ràng buộc ngang hàng vớirequired, trong khi một cái chạy trong ứng dụng, một cái do máy chủ thực thi, và không có xử lý lỗi nối nào. Cú phápimportcần"type": "module"hoặc đuôi.mjs[Suy luận: theo tài liệu Node, bài chạy bằng CommonJS vớirequire]. - Giới hạn của bài. Chưa nói kết nối có xác thực (
URIcó user và mật khẩu, chương 53),TLS, connection pool, và chạy nhiều tiến trình [Suy luận: các chủ đề này không chạy trong bài]. - Đọc thêm. Tài liệu Mongoose: https://mongoosejs.com/docs/connections.html và https://mongoosejs.com/docs/validation.html
Bài tiếp theo
Bài sau là Form đăng ký với Node.js và MongoDB: nhận email và mật khẩu qua HTTP, băm mật khẩu bằng bcrypt, lưu vào shop, và trả đúng mã lỗi khi email đã tồn tại. Mini-dự án nối tiếp: cho khách của Tạp Hóa Cô Tấm tạo tài khoản đầu tiên.
Lời giải và đáp án
Làm cùng mình, Bước 2. Ba chỗ trống đã điền:
const mongoose = require("mongoose");
(async () => {
await mongoose.connect(process.env.MONGODB_URI);
console.log("state:", mongoose.connection.readyState, "database:", mongoose.connection.name);
await mongoose.disconnect();
console.log("closed:", mongoose.connection.readyState);
})();
printf 'MONGODB_URI=mongodb://127.0.0.1:27017/shop\n' > .env
node --env-file=.env mine.js
state: 1 database: shop
closed: 0
Nối bằng URI từ biến môi trường, in 1 và shop, rồi đóng kết nối nên readyState về 0. Câu hỏi thêm: bỏ dòng đóng thì tiến trình không thoát, vì kết nối đang mở giữ vòng lặp sự kiện của Node, như phần Quên đóng kết nối đã chạy.
Bài 1.
const mongoose = require("mongoose");
const productSchema = new mongoose.Schema({
name: { type: String, required: true, minlength: 2 },
price: { type: Number, required: true, min: 0 },
sku: { type: String, required: true, unique: true },
});
module.exports = mongoose.model("Product", productSchema);
const Product = require("./product");
(async () => {
try {
await new Product({ name: "rice", price: -5 }).validate();
} catch (err) {
console.log(err.name, "on:", Object.keys(err.errors).sort().join(", "));
}
await new Product({ name: "rice", price: 18000, sku: "R-01" }).validate();
console.log("valid product passes");
})();
node ex_schema.js
ValidationError on: price, sku
valid product passes
Thiếu sku và price âm đều bị bắt; cả hai nằm trong errors. Một document đủ ba field đi qua validate() mà không ném gì.
Bài 2.
const mongoose = require("mongoose");
mongoose.set("bufferTimeoutMS", 1500);
const User = mongoose.model("User", new mongoose.Schema({ email: String }));
(async () => {
const started = Date.now();
try {
await User.find();
} catch (err) {
console.log(err.message);
console.log("failed within 3 seconds:", Date.now() - started < 3000);
}
})();
node ex_buffer.js
Operation `users.find()` buffering timed out after 1500ms
failed within 3 seconds: true
bufferTimeoutMS thay con số 10000 mặc định. Lệnh vẫn nằm trong hàng chờ rồi mới lỗi, chỉ là sớm hơn.
Bài 3.
const mongoose = require("mongoose");
const { connect } = require("./db");
const schema = new mongoose.Schema({ email: { type: String, unique: true } }, { autoIndex: false });
const Loose = mongoose.model("Loose", schema, "loose_users");
(async () => {
await connect();
await Loose.insertMany([{ email: "an@shop.com" }, { email: "an@shop.com" }, { email: "binh@shop.com" }]);
const groups = await Loose.aggregate([{ $group: { _id: "$email", ids: { $push: "$_id" }, n: { $sum: 1 } } }, { $match: { n: { $gt: 1 } } }]);
console.log("duplicate groups:", groups.length);
for (const g of groups) await Loose.deleteMany({ _id: { $in: g.ids.slice(1) } });
await Loose.createIndexes();
console.log("documents left:", await Loose.countDocuments());
console.log("indexes:", Object.keys(await Loose.collection.getIndexes()).join(", "));
try {
await Loose.create({ email: "binh@shop.com" });
} catch (err) {
console.log("duplicate now blocked: code", err.code);
}
await mongoose.disconnect();
})();
printf 'MONGODB_URI=mongodb://127.0.0.1:27017/shop\n' > .env
node --env-file=.env ex_dedupe.js
duplicate groups: 1
documents left: 2
indexes: _id_, email_1
duplicate now blocked: code 11000
$group gom các _id theo email, nhóm có hơn một phần tử là nhóm trùng; giữ phần tử đầu, xóa phần còn lại bằng deleteMany. Sau khi dữ liệu sạch, createIndexes() dựng được email_1 và lần tạo trùng bị mã 11000.
Tự kiểm tra.
- 2, trạng thái đang nối.
URIthật có mật khẩu; mã nguồn thì được đẩy lên kho chung, còn.envthì không.products.uniquechỉ là index; ứng dụng vẫn phải bắterr.code === 11000để trả lời khách cho tử tế, và nếu index chưa được tạo thì còn không chặn gì.- Hàng chờ che lỗi cấu hình bằng 10 giây im lặng;
awaitnối xong rồi mới nhận request thì lỗi nổ ngay lúc khởi động.