# Bài 03 — MongoDB căn bản & nạp dữ liệu mẫu
⬅️ Bài trước | Mục lục | Bài tiếp theo ➡️
🎯 Mục tiêu
- Hiểu MongoDB khác SQL ở chỗ nào
- Nắm 4 khái niệm: database, collection, document,
ObjectId - Viết được truy vấn
find,findOne,insertOne,updateOne,deleteOne - Tạo xong database
lith_perfumevới đủ 3 collection và dữ liệu mẫu
Cuối bài bạn sẽ có database sẵn sàng cho backend ở bài 04.
📚 1. MongoDB là gì?
MongoDB là cơ sở dữ liệu NoSQL dạng document. Thay vì bảng và dòng như MySQL, nó lưu dữ liệu dưới dạng các "tài liệu" trông y hệt object JavaScript.
So sánh với SQL
| SQL (MySQL, PostgreSQL) | MongoDB | Ví dụ trong dự án |
|---|---|---|
| Database | Database | lith_perfume |
| Table (bảng) | Collection | products, categories, orders |
| Row (dòng) | Document | Một sản phẩm |
| Column (cột) | Field | name, price, rating |
| Schema cố định | Không bắt buộc schema | Sản phẩm A có size, sản phẩm B không cũng được |
JOIN |
Không có (phải tự ghép hoặc dùng $lookup) |
Bài 06 sẽ ghép thủ công |
Một document trông như thế nào?
{
"_id": ObjectId("6675905ecb0dded448a58bb0"),
"name": "Gucci Flora Gorgeous Magnolia",
"image": "sp1.jpg",
"price": 5050000,
"description": "Hương đầu: Quả mâm xôi, Dừa...",
"rating": 4.5,
"categoryId": "669a978e6e282b058a8e3f2c",
"size": "100ml"
}
Đúng là một object JavaScript. Đây là lý do MongoDB rất hợp với dự án Node.js: không cần "dịch" qua lại giữa object và bảng.
📚 2. _id và ObjectId — khái niệm gây nhầm lẫn nhất
Mọi document đều có _id
Khi bạn thêm một document mà không chỉ định _id, MongoDB tự sinh một ObjectId.
ObjectId là gì?
Một chuỗi 24 ký tự hex, nhưng không phải kiểu chuỗi — nó là một kiểu dữ liệu riêng.
6675905ecb0dded448a58bb0
└──┬──┘└─┬──┘└┬─┘└─┬──┘
│ │ │ └── Bộ đếm tăng dần (3 byte)
│ │ └─────── ID tiến trình (2 byte)
│ └──────────── ID máy (5 byte)
└────────────────── Thời gian tạo (4 byte)
Vì có timestamp bên trong, ObjectId tự nhiên sắp xếp theo thời gian tạo.
⚠️ Cạm bẫy số 1: chuỗi ≠ ObjectId
// ❌ SAI — trả về null, dù _id đó có tồn tại
db.collection("products").findOne({ _id: "6675905ecb0dded448a58bb0" });
// ✅ ĐÚNG — phải chuyển sang ObjectId
const { ObjectId } = require("mongodb");
db.collection("products").findOne({ _id: new ObjectId("6675905ecb0dded448a58bb0") });
Đây là lỗi số 1 của người mới học MongoDB. Query không báo lỗi, chỉ đơn giản là không tìm thấy gì cả — nên rất khó phát hiện.
⚠️ Cạm bẫy số 2 (lỗi thật trong dự án này)
Trong file dữ liệu mẫu products.txt:
{
"_id": { "$oid": "6675905ecb0dded448a58bb0" }, ← sẽ thành ObjectId khi import
"categoryId": "669a978e6e282b058a8e3f2c" ← chỉ là CHUỖI thường
}
Còn trong categories.json:
{
"_id": { "$oid": "669a978e6e282b058a8e3f2c" } ← là ObjectId
}
Nghĩa là products.categoryId (chuỗi) không khớp kiểu với categories._id (ObjectId).
Hậu quả trong code gốc:
// backend/routes/products.js — route lọc theo danh mục
router.get("/byCategory/:id", async (req, res, next) => {
const products = await productCollection
.find({ categoryId: new ObjectId(req.params.id) }) // ← tìm ObjectId
.toArray(); // nhưng DB lưu chuỗi
// → LUÔN trả về mảng rỗng []
});
Trong khi route GET /products lại may mắn đúng, vì nó so sánh sau khi ép về chuỗi:
const category = categories.find(
(cat) => cat._id.toString() === item.categoryId.toString() // ← cả 2 đều về chuỗi
);
Cách xử lý — chọn 1 trong 2 (tài liệu này chọn cách A):
| Cách A: Chuẩn hóa dữ liệu | Cách B: Sửa code | |
|---|---|---|
| Làm gì | Import với categoryId là {"$oid": "..."} để thành ObjectId |
Sửa query thành { categoryId: req.params.id } (giữ chuỗi) |
| Ưu | Đúng chuẩn MongoDB, có thể dùng $lookup sau này |
Không cần sửa dữ liệu |
| Nhược | Phải sửa file dữ liệu trước khi import | Sai chuẩn, khó mở rộng |
Phần 5 dưới đây sẽ hướng dẫn cách A cụ thể.
📚 3. Các truy vấn cơ bản
Bạn có thể gõ trực tiếp trong MongoDB Compass (tab >_MONGOSH ở dưới cùng)
hoặc dùng mongosh trong terminal.
Trước tiên chọn database:
use lith_perfume
find() — lấy nhiều document
// Lấy tất cả
db.products.find()
// Lọc theo điều kiện
db.products.find({ rating: 4.5 })
// Giá > 4 triệu
db.products.find({ price: { $gt: 4000000 } })
// Giá trong khoảng 3-5 triệu
db.products.find({ price: { $gte: 3000000, $lte: 5000000 } })
// Tên chứa chữ "Gucci" (không phân biệt hoa thường)
db.products.find({ name: /Gucci/i })
Các toán tử so sánh
| Toán tử | Nghĩa | Ví dụ |
|---|---|---|
$gt |
greater than — lớn hơn | { price: { $gt: 100 } } |
$gte |
greater or equal — lớn hơn hoặc bằng | { price: { $gte: 100 } } |
$lt |
less than — nhỏ hơn | { price: { $lt: 100 } } |
$lte |
less or equal | { price: { $lte: 100 } } |
$ne |
not equal — khác | { rating: { $ne: 0 } } |
$in |
nằm trong danh sách | { size: { $in: ["S", "M"] } } |
$regex |
khớp biểu thức chính quy | { name: { $regex: "Gucci", $options: "i" } } |
findOne() — lấy đúng 1 document
db.products.findOne({ _id: ObjectId("6675905ecb0dded448a58bb0") })
Khác biệt: find() trả về một con trỏ (phải .toArray() mới ra mảng),
còn findOne() trả về thẳng 1 object hoặc null.
sort(), limit(), skip()
// 10 sản phẩm đánh giá cao nhất
db.products.find().sort({ rating: -1 }).limit(10)
// Trang 2, mỗi trang 5 sản phẩm
db.products.find().skip(5).limit(5)
sort: 1 = tăng dần, -1 = giảm dần.
insertOne() / insertMany()
db.categories.insertOne({ name: "Dior", image: "h11.png" })
db.categories.insertMany([
{ name: "Chanel", image: "h12.png" },
{ name: "Hermès", image: "h13.png" }
])
updateOne() / updateMany()
db.products.updateOne(
{ _id: ObjectId("6675905ecb0dded448a58bb0") }, // tìm cái nào
{ $set: { price: 4990000 } } // sửa gì
)
// Tăng rating thêm 0.5 cho tất cả sản phẩm
db.products.updateMany({}, { $inc: { rating: 0.5 } })
⚠️ Bắt buộc phải có
$set. Nếu viếtupdateOne({...}, { price: 100 })MongoDB sẽ báo lỗi. Còn nếu dùngreplaceOne, document sẽ bị thay hoàn toàn — mọi field khác biến mất.
deleteOne() / deleteMany()
db.products.deleteOne({ _id: ObjectId("...") })
db.products.deleteMany({ rating: 0 })
db.products.deleteMany({}) // ⚠️ Xóa SẠCH collection
countDocuments()
db.products.countDocuments() // đếm tất cả
db.products.countDocuments({ rating: 4.5 }) // đếm theo điều kiện
💻 4. Tạo database lith_perfume
Cách 1 — Bằng MongoDB Compass (khuyến nghị cho người mới)
- Mở Compass, connect vào
mongodb://localhost:27017 - Bấm nút + cạnh chữ "Databases" (hoặc Create database)
- Điền:
- Database Name:
lith_perfume - Collection Name:
categories
- Database Name:
- Bấm Create Database
- Bên trong
lith_perfume, bấm Create collection thêm 2 lần để tạoproductsvàorders
Cách 2 — Bằng mongosh
use lith_perfume
db.createCollection("categories")
db.createCollection("products")
db.createCollection("orders")
💡 MongoDB không tạo database rỗng. Database chỉ thực sự tồn tại khi có ít nhất 1 collection chứa ít nhất 1 document. Đừng hoảng nếu chưa thấy nó xuất hiện.
💻 5. Nạp dữ liệu mẫu
Bước 5.1 — Chuẩn bị file categories.json
File này đã đúng định dạng, import thẳng được:
[
{ "_id": { "$oid": "66758da7cb0dded448a58ba7" }, "name": "Valentino", "image": "h3.png" },
{ "_id": { "$oid": "66758e26cb0dded448a58ba8" }, "name": "Chloé", "image": "h5.png" },
{ "_id": { "$oid": "669a970b6e282b058a8e3f27" }, "name": "Jean Paul Gaultier", "image": "h1.png" },
{ "_id": { "$oid": "669a97426e282b058a8e3f28" }, "name": "Versace", "image": "h2.png" },
{ "_id": { "$oid": "669a97596e282b058a8e3f29" }, "name": "Ralph Lauren", "image": "h4.png" },
{ "_id": { "$oid": "669a97706e282b058a8e3f2a" }, "name": "Burberry", "image": "h6.png" },
{ "_id": { "$oid": "669a977c6e282b058a8e3f2b" }, "name": "Narciso Rodriguez", "image": "h7.png" },
{ "_id": { "$oid": "669a978e6e282b058a8e3f2c" }, "name": "Gucci", "image": "h8.png" },
{ "_id": { "$oid": "669cf8e86e282b058a8e3f46" }, "name": "Tom Ford", "image": "h9.png" },
{ "_id": { "$oid": "669cfa0f6e282b058a8e3f47" }, "name": "Kilian", "image": "h10.png" }
]
💡
{ "$oid": "..." }là cú pháp Extended JSON — báo cho MongoDB biết "hãy chuyển chuỗi này thành ObjectId, đừng lưu như chuỗi thường".
Bước 5.2 — Sửa products.txt để categoryId thành ObjectId
Đây là bước quan trọng để tránh lỗi đã nói ở mục 2.
File gốc có dạng:
{
"_id": { "$oid": "6675905ecb0dded448a58bb0" },
"name": "Gucci Flora Gorgeous Magnolia",
"categoryId": "669a978e6e282b058a8e3f2c"
}
Cần đổi thành:
{
"_id": { "$oid": "6675905ecb0dded448a58bb0" },
"name": "Gucci Flora Gorgeous Magnolia",
"categoryId": { "$oid": "669a978e6e282b058a8e3f2c" }
}
Cách làm nhanh bằng VS Code (Find & Replace với Regex):
- Mở
products.txttrong VS Code Ctrl + Hđể mở Replace- Bật nút
.*(Use Regular Expression) - Find:
"categoryId": "([a-f0-9]{24})" - Replace:
"categoryId": { "$oid": "$1" } - Bấm Replace All (
Ctrl + Alt + Enter)
Lưu file lại. Bạn nên đổi tên thành products.json cho đúng định dạng.
Hoặc dùng lệnh (PowerShell):
(Get-Content products.txt -Raw) -replace '"categoryId": "([a-f0-9]{24})"', '"categoryId": { "$oid": "$1" }' | Set-Content products.json -Encoding utf8
Bước 5.3 — Import vào Compass
Với từng collection:
- Trong Compass, mở database
lith_perfume→ chọn collectioncategories - Bấm ADD DATA → Import JSON or CSV file
- Chọn file
categories.json - Input File Type: JSON
- Bấm Import
Lặp lại cho products với file products.json vừa sửa.
Bước 5.4 — Import bằng dòng lệnh (cách khác)
Nếu bạn có mongoimport (đi kèm MongoDB Database Tools):
mongoimport --db lith_perfume --collection categories --file categories.json --jsonArray
mongoimport --db lith_perfume --collection products --file products.json --jsonArray
Cờ --jsonArray là bắt buộc vì file của ta là một mảng [...].
✅ 6. Kiểm tra dữ liệu đã đúng chưa
Mở tab >_MONGOSH trong Compass và chạy lần lượt:
use lith_perfume
// 1. Phải ra 10
db.categories.countDocuments()
// 2. Phải ra 12
db.products.countDocuments()
// 3. Kiểm tra kiểu của categoryId — QUAN TRỌNG NHẤT
db.products.findOne()
Nhìn vào kết quả câu 3, categoryId phải hiển thị là:
categoryId: ObjectId('669a978e6e282b058a8e3f2c') // ✅ Đúng
Chứ không phải:
categoryId: '669a978e6e282b058a8e3f2c' // ❌ Vẫn là chuỗi, làm lại bước 5.2
Kiểm tra thêm bằng cách đếm sản phẩm của một danh mục:
// 4. Phải ra số > 0 (Gucci có 2 sản phẩm)
db.products.countDocuments({ categoryId: ObjectId("669a978e6e282b058a8e3f2c") })
Nếu ra 0 → categoryId vẫn đang là chuỗi. Quay lại bước 5.2.
Cách sửa nhanh nếu đã lỡ import sai
Không cần import lại. Chạy script này trong mongosh:
use lith_perfume
db.products.find({ categoryId: { $type: "string" } }).forEach(function (doc) {
db.products.updateOne(
{ _id: doc._id },
{ $set: { categoryId: new ObjectId(doc.categoryId) } }
);
});
// Kiểm tra lại
db.products.countDocuments({ categoryId: { $type: "string" } }); // phải ra 0
📚 7. Ghép dữ liệu giữa 2 collection
MongoDB không có JOIN như SQL. Có 2 cách ghép:
Cách 1 — Ghép trong code (dự án dùng cách này)
// Lấy hết sản phẩm, lấy hết danh mục, rồi ghép bằng JavaScript
const products = await db.collection("products").find().toArray();
const categories = await db.collection("categories").find().toArray();
products.forEach((p) => {
p.category = categories.find((c) => c._id.toString() === p.categoryId.toString());
});
✅ Dễ hiểu · ❌ Tải hết dữ liệu về mới ghép, chậm khi dữ liệu lớn
Cách 2 — Dùng $lookup (aggregation)
db.products.aggregate([
{
$lookup: {
from: "categories", // ghép với collection nào
localField: "categoryId", // field bên products
foreignField: "_id", // field bên categories
as: "category" // kết quả đặt vào field tên gì
}
},
{ $unwind: "$category" } // biến mảng 1 phần tử thành object
])
✅ Nhanh, MongoDB làm hết · ❌ Cú pháp khó nhớ hơn
⚠️ Chỉ chạy đúng khi categoryId là ObjectId — thêm một lý do phải làm bước 5.2.
Thử ngay trong mongosh để thấy kết quả:
db.products.aggregate([
{ $lookup: { from: "categories", localField: "categoryId", foreignField: "_id", as: "category" } },
{ $unwind: "$category" },
{ $project: { name: 1, price: 1, "category.name": 1 } }
])
Tài liệu chọn cách 1 cho bài 06 vì dễ hiểu hơn với người mới, nhưng bài 21 sẽ giới thiệu
lại $lookup khi làm phân trang.
📚 8. Index — tăng tốc truy vấn
Khi dữ liệu lớn, truy vấn không có index sẽ phải quét toàn bộ collection.
// Tạo index cho categoryId (vì hay lọc theo nó)
db.products.createIndex({ categoryId: 1 })
// Index cho tìm kiếm theo tên
db.products.createIndex({ name: "text" })
// Xem các index hiện có
db.products.getIndexes()
Với 12 sản phẩm thì chưa cần. Nhưng bài 21 (tìm kiếm & phân trang) sẽ dùng đến.
📝 Bài tập
Chạy trong mongosh, tự viết trước khi xem đáp án:
- Đếm số sản phẩm có giá trên 4.000.000đ
- Lấy 3 sản phẩm đắt nhất, chỉ hiện
namevàprice - Tìm tất cả sản phẩm có chữ "Eau" trong tên (không phân biệt hoa thường)
- Tăng giá tất cả sản phẩm của Gucci lên 10%
- Đếm số sản phẩm theo từng danh mục (gợi ý: dùng
aggregatevới$group)
// 1
db.products.countDocuments({ price: { $gt: 4000000 } })
// 2
db.products.find({}, { name: 1, price: 1, _id: 0 }).sort({ price: -1 }).limit(3)
// 3
db.products.find({ name: /Eau/i })
// 4
db.products.updateMany(
{ categoryId: ObjectId("669a978e6e282b058a8e3f2c") },
{ $mul: { price: 1.1 } }
)
// 5
db.products.aggregate([
{ $group: { _id: "$categoryId", soLuong: { $sum: 1 } } },
{ $lookup: { from: "categories", localField: "_id", foreignField: "_id", as: "cat" } },
{ $unwind: "$cat" },
{ $project: { _id: 0, danhMuc: "$cat.name", soLuong: 1 } },
{ $sort: { soLuong: -1 } }
])
</details>
✅ Checklist trước khi sang bài 04
- [ ] Database
lith_perfumetồn tại - [ ] Collection
categoriescó 10 document - [ ] Collection
productscó 12 document - [ ] Collection
orderstồn tại (rỗng cũng được) - [ ]
db.products.findOne().categoryIdhiển thị làObjectId(...), không phải chuỗi - [ ]
db.products.countDocuments({ categoryId: { $type: "string" } })trả về 0
⬅️ Bài trước | Mục lục | Bài tiếp theo: Khởi tạo dự án Express ➡️
All rights reserved