Một cửa hàng bán laptop, sách và quần áo cần lưu những thuộc tính rất khác nhau. Laptop có RAM và CPU; sách có tác giả; quần áo có kích cỡ. Nếu bạn đang học cơ sở dữ liệu, đây là tình huống dễ hình dung để bắt đầu với MongoDB.
Bài học này dùng một danh mục sản phẩm nhỏ để hướng dẫn cách tổ chức dữ liệu và thực hiện bốn thao tác CRUD. Bạn sẽ đọc kết quả sau từng bước, xử lý lỗi thường gặp và hiểu giới hạn của ví dụ trước khi áp dụng vào dự án thật.
1. Bạn sẽ học được gì?
- Phân biệt database, collection, document và field.
- Thêm, tìm, sửa, xóa sản phẩm bằng mongosh.
- Truy vấn thuộc tính lồng nhau, chọn trường và sắp xếp kết quả.
- Trừ tồn kho bằng cập nhật có điều kiện.
- Nhận biết khi nào cần xem lại mô hình dữ liệu hoặc cân nhắc cơ sở dữ liệu quan hệ.
Đối tượng: người mới học Data Engineering hoặc backend, biết thao tác terminal cơ bản. Bạn không cần biết JavaScript chuyên sâu. Môi trường minh họa là MongoDB Community 8.0 và mongosh đi kèm trong image Docker; đây là phiên bản cố định cho bài lab, không phải khẳng định phiên bản mới nhất. Chạy lần lượt các ví dụ trên dữ liệu mẫu để kết quả khớp bài.
Mục lục
- MongoDB và mô hình document
- Chuẩn bị môi trường thực hành
- Create: thêm dữ liệu mẫu
- Read: tìm và đọc sản phẩm
- Update: cập nhật và trừ tồn kho
- Delete: xóa có kiểm soát
- Index và mã sản phẩm duy nhất
- Lỗi thường gặp
- Khi nào nên dùng MongoDB?
- Bài tập và gợi ý lời giải
- Tổng kết và học tiếp
- Tài liệu tham khảo
2. MongoDB và mô hình document
MongoDB là cơ sở dữ liệu hướng document. Một document gồm các cặp tên trường và giá trị, có thể chứa đối tượng lồng nhau hoặc mảng. Dữ liệu được lưu dưới dạng BSON, hỗ trợ nhiều kiểu hơn JSON. Trong bài, chúng ta biểu diễn sản phẩm bằng cú pháp đối tượng dễ đọc trong mongosh. [1]
| Khái niệm | Vai trò trong bài lab |
|---|---|
| Database | Không gian dữ liệu techdata_mongodb_lab |
| Collection | Nhóm sản phẩm products |
| Document | Một sản phẩm cụ thể |
| Field | Thuộc tính như name, price_vnd, stock |
| _id | Định danh duy nhất của document trong collection |
Để hình dung, database là tủ hồ sơ, collection là một ngăn và document là một hồ sơ. Đây là phép so sánh để học khái niệm, không có nghĩa MongoDB lưu dữ liệu thành các tệp hồ sơ riêng biệt như vậy.
Các document trong cùng collection có thể khác trường. Tuy nhiên, schema linh hoạt vẫn cần quy ước về tên, kiểu và ý nghĩa dữ liệu. MongoDB hỗ trợ schema validation để kiểm soát cấu trúc khi cần. Việc nhúng dữ liệu hay tham chiếu sang collection khác nên xuất phát từ cách ứng dụng đọc và cập nhật dữ liệu. [2]
Với cửa hàng mẫu, ta quy ước mọi sản phẩm đều có mã, tên, nhóm hàng, giá và tồn kho; thuộc tính riêng nằm trong specs. Nhờ đó, người đọc biết phần nào phải thống nhất và phần nào được thay đổi theo loại hàng.

3. Chuẩn bị môi trường thực hành
Kiểm tra Docker trong terminal của máy bạn:
docker --version
Nếu chưa có Docker, hãy hoàn tất cài đặt theo tài liệu chính thức phù hợp hệ điều hành trước khi tiếp tục. Tạo một MongoDB dùng riêng cho bài học:
docker run -d --name techdata-mongo-lab -v techdata_mongo_lab_data:/data/db mongo:8.0
Lệnh này chạy container ở nền và dùng named volume để giữ dữ liệu tại /data/db. Bài lab không mở cổng ra máy chủ; bạn sẽ truy cập trực tiếp trong container. Cấu hình không xác thực này chỉ dành cho thực hành cục bộ, không dùng làm cấu hình triển khai dịch vụ thật.
docker exec techdata-mongo-lab mongosh --quiet --eval 'db.runCommand({ ping: 1 })'
docker exec -it techdata-mongo-lab mongosh
Lệnh ping cần trả về kết quả có ok: 1. Nếu MongoDB vừa khởi động chưa sẵn sàng, kiểm tra docker logs techdata-mongo-lab rồi thử lại sau khi tiến trình đã nhận kết nối.
Phân biệt nơi chạy lệnh: các lệnh bắt đầu bằng docker chạy ở terminal hệ điều hành. Từ bước dưới đây, các lệnh use và db chạy trong mongosh vừa mở.
use techdata_mongodb_lab
db.getName()
Kết quả tên database cần là techdata_mongodb_lab. Khi học, hãy kiểm tra tên này trước các lệnh ghi dữ liệu để tránh thao tác nhầm nơi.
4. Create: thêm dữ liệu mẫu
CRUD viết tắt của Create, Read, Update và Delete: tạo, đọc, cập nhật, xóa dữ liệu. MongoDB cung cấp các phương thức tương ứng như insertMany, find, updateOne và deleteOne. [3]
db.products.insertMany([
{
_id: "P001",
sku: "LAP-001",
name: "Laptop học Data",
category: "laptop",
price_vnd: 15000000,
stock: 2,
specs: { ram_gb: 16, cpu: "Core i5" },
tags: ["data", "study"]
},
{
_id: "P002",
sku: "BOOK-001",
name: "Sách SQL cơ bản",
category: "book",
price_vnd: 200000,
stock: 10,
specs: { author: "Tác giả mẫu", pages: 250 },
tags: ["sql", "study"]
},
{
_id: "P003",
sku: "SHIRT-001",
name: "Áo thun TechData",
category: "clothing",
price_vnd: 250000,
stock: 5,
specs: { size: "L", color: "pink" },
tags: ["merch"]
}
])
Khi thao tác thành công, phản hồi có acknowledged bằng true và danh sách các _id vừa thêm. Kiểm tra số lượng:
db.products.countDocuments({})
Kết quả mong đợi: 3. Ba sản phẩm dùng cùng cấu trúc lõi nhưng phần specs khác nhau. Giá được lưu bằng số nguyên theo đơn vị VND để bài tập dễ theo dõi; đây là quy ước của dữ liệu mẫu.
Bài chủ động đặt _id dạng chuỗi để bạn gõ lại dễ dàng. Nếu chạy lại insertMany với các _id đã tồn tại, bạn sẽ gặp lỗi trùng khóa. Đừng coi việc thêm lại dữ liệu là thao tác làm mới tự động.
5. Read: tìm và đọc sản phẩm
5.1. Đọc toàn bộ dữ liệu mẫu
db.products.find({})
Đối số đầu là bộ lọc. Bộ lọc rỗng nghĩa là không đặt điều kiện. Phương thức find trả về cursor; mongosh hiển thị kết quả để bạn xem. Với dữ liệu lớn, không nên tùy tiện lấy toàn bộ về ứng dụng. [4]
5.2. Lọc theo nhóm và chọn trường cần đọc
db.products.find(
{ category: "laptop" },
{ _id: 0, sku: 1, name: 1, price_vnd: 1 }
)
Đối số thứ hai là projection, chỉ định trường trả về. Số 1 giữ trường; _id bằng 0 ẩn định danh khỏi kết quả. [4] Với dữ liệu vừa nhập, bạn thấy một laptop có mã LAP-001 và giá 15.000.000 VND.
5.3. Kết hợp điều kiện và sắp xếp
db.products.find(
{ price_vnd: { $lte: 300000 }, stock: { $gt: 0 } },
{ _id: 0, sku: 1, price_vnd: 1, stock: 1 }
).sort({ price_vnd: 1, sku: 1 })
Đọc câu lệnh bằng lời: tìm sản phẩm giá không quá 300.000 VND và còn hàng, chỉ hiển thị mã, giá, tồn kho, rồi sắp xếp giá tăng dần. Mã sản phẩm là tiêu chí phụ nếu giá bằng nhau.
Kết quả mong đợi: BOOK-001 có giá 200.000 và tồn 10, tiếp theo SHIRT-001 có giá 250.000 và tồn 5. Hãy đối chiếu cả số dòng lẫn giá trị; một truy vấn chạy không lỗi chưa chắc đã lọc đúng nghiệp vụ.
5.4. Đọc trường lồng nhau và mảng
db.products.find({ "specs.ram_gb": { $gte: 16 } })
db.products.find({ tags: "study" })
Truy vấn thứ nhất tìm laptop RAM từ 16 GB. Truy vấn thứ hai tìm các sản phẩm có phần tử study trong mảng tags. Kết quả tương ứng là một laptop và hai sản phẩm laptop, sách. Dùng đường dẫn có dấu chấm để truy cập trường nằm trong specs. [4]
6. Update: cập nhật và trừ tồn kho
6.1. Đổi giá một sản phẩm
db.products.updateOne(
{ _id: "P002" },
{ $set: { price_vnd: 220000 } }
)
db.products.findOne({ _id: "P002" })
Bộ lọc xác định sản phẩm; $set chỉ định giá mới. updateOne cập nhật một document khớp bộ lọc. Phản hồi có matchedCount và modifiedCount để phân biệt tìm thấy với thực sự thay đổi. [5]
Lần đầu, mong đợi matchedCount bằng 1, modifiedCount bằng 1. Nếu chạy lại khi giá đã là 220.000, matchedCount vẫn có thể bằng 1 nhưng modifiedCount bằng 0. Vì vậy, đừng hiểu mọi modifiedCount bằng 0 là lỗi kết nối.
6.2. Chỉ trừ kho khi đủ số lượng
Giả sử một khách cần mua một laptop. Ta đặt điều kiện còn ít nhất một sản phẩm ngay trong bộ lọc và giảm stock bằng $inc:
db.products.updateOne(
{ _id: "P001", stock: { $gte: 1 } },
{ $inc: { stock: -1 } }
)
db.products.findOne({ _id: "P001" }, { stock: 1 })
Nếu chạy đúng một lần, tồn kho từ 2 còn 1. Nếu chạy thêm lần nữa, tồn về 0. Chạy lần thứ ba không khớp điều kiện; matchedCount bằng 0 và stock vẫn bằng 0.
Một thao tác ghi trên một document có tính nguyên tử. Đặt điều kiện tồn kho và phép giảm trong cùng lệnh giúp tránh khoảng trống giữa đọc rồi ghi ở ứng dụng. Điều này không biến các bước tạo đơn, trừ nhiều sản phẩm và thanh toán thành một giao dịch nguyên tử. updateMany cũng không nguyên tử cho toàn bộ tập document; MongoDB hỗ trợ transaction nhiều document trên cấu hình phù hợp như replica set. [6]
Giới hạn của lab: container standalone ở trên chỉ minh họa cập nhật một document. Bài không thực hiện thanh toán, không triển khai transaction nhiều document và chưa giải quyết việc cùng yêu cầu mua hàng bị gửi lặp. Trong dự án thật, đó là các bài toán phải thiết kế riêng.
7. Delete: xóa có kiểm soát
Để không làm thay đổi ba sản phẩm chính của bài tập, tạo một sản phẩm tạm và xóa đúng định danh đó:
db.products.insertOne({
_id: "TEMP-001",
sku: "TEMP-001",
name: "Sản phẩm thử xóa",
category: "test",
price_vnd: 0,
stock: 0
})
db.products.findOne({ _id: "TEMP-001" })
db.products.deleteOne({ _id: "TEMP-001" })
db.products.countDocuments({ _id: "TEMP-001" })
Phản hồi xóa cần có deletedCount bằng 1; phép đếm cuối bằng 0. Nếu chạy lại lệnh xóa, deletedCount bằng 0 vì document đã không còn. Trong thực hành, luôn đọc lại bộ lọc trước khi xóa; tránh dùng bộ lọc rỗng theo thói quen.
8. Index và mã sản phẩm duy nhất
Ta muốn mã sku không bị lặp giữa các sản phẩm. Tạo unique index:
db.products.createIndex({ sku: 1 }, { unique: true })
db.products.getIndexes()
createIndex tạo chỉ mục theo trường được chỉ định. Tùy chọn unique yêu cầu giá trị khóa chỉ mục không trùng nhau. Với dữ liệu mẫu, chỉ mục trên sku tạo được vì các mã đều khác nhau. [7]
Kiểm tra quy tắc bằng cách thử thêm một sản phẩm mới với sku đã dùng:
db.products.insertOne({
_id: "P004",
sku: "LAP-001",
name: "Sản phẩm trùng mã"
})
Kết quả mong đợi: lỗi E11000 duplicate key; P004 không được thêm. Đây là lỗi có chủ đích để học cách cơ sở dữ liệu từ chối dữ liệu vi phạm. Không xóa index chỉ để làm cho dữ liệu sai được ghi vào.
Với ba sản phẩm, chưa có cơ sở kết luận index làm ứng dụng nhanh hơn bao nhiêu. Khi dữ liệu lớn, hãy đo truy vấn thực tế và chi phí ghi trước khi thêm nhiều chỉ mục. Tạo index là một quyết định thiết kế cần kiểm chứng bằng workload, không phải bước trang trí cho bài SQL hay NoSQL.
9. Lỗi thường gặp và cách xử lý
| Dấu hiệu | Cách kiểm tra | Cách xử lý |
|---|---|---|
| docker: command not found | Chạy docker --version ở terminal | Cài Docker và bảo đảm dịch vụ hoạt động |
| Container name already in use | Chạy docker ps -a | Nếu đúng container lab cũ, dùng docker start techdata-mongo-lab |
| Không tìm thấy sản phẩm | Kiểm tra db.getName(), tên collection và bộ lọc | Chọn lại database, đối chiếu _id dạng chuỗi |
| E11000 khi nhập mẫu lần nữa | Đọc tên index và giá trị bị trùng trong lỗi | Dùng dữ liệu hiện có; nếu muốn học lại, chọn database lab mới và khởi tạo từ đầu |
| matchedCount bằng 0 | Đọc lại _id và điều kiện stock | Phân biệt không có sản phẩm với sản phẩm đã hết hàng |
| Giá lọc sai mong đợi | Kiểm tra giá đang là số hay chuỗi | Thống nhất kiểu số; tránh nhập giá kiểu “220.000đ” vào trường tính toán |
Khi đọc lỗi, hãy ghi lại lệnh vừa chạy, database hiện tại và dữ liệu trước thao tác. Ba thông tin này hữu ích hơn việc chỉ chụp một dòng thông báo cuối.
10. Khi nào nên dùng MongoDB?
Với ví dụ cửa hàng, hãy bắt đầu từ các màn hình và thao tác cần hỗ trợ: trang chi tiết cần những dữ liệu nào, thuộc tính nào hay đổi, dữ liệu liên quan lớn đến đâu? Sau đó mới chọn mô hình.
| Nhu cầu dự án | Hướng cân nhắc |
|---|---|
| Danh mục có thuộc tính khác nhau theo loại hàng | Thử mô hình document với phần lõi thống nhất và specs riêng |
| Dữ liệu con nhỏ, giới hạn và thường đọc cùng dữ liệu cha | Đánh giá embedding, đồng thời xem chi phí cập nhật |
| Bình luận tăng liên tục hoặc có vòng đời độc lập | Đánh giá collection riêng và tham chiếu |
| Nhiều quan hệ chéo, báo cáo SQL và quy tắc nhất quán phức tạp | So sánh với PostgreSQL hoặc hệ quan hệ bằng truy vấn thực tế |
Đây là định hướng thiết kế cho bài học, không phải bảng xếp hạng công nghệ. Trước khi chọn cho sản phẩm thật, hãy dựng một lát cắt nghiệp vụ, nhập dữ liệu gần thực tế và đo cả đọc lẫn ghi. Đừng chọn MongoDB chỉ vì dữ liệu đầu vào là JSON; cũng đừng loại nó chỉ vì ứng dụng có quan hệ giữa các thực thể.
11. Bài tập tự luyện và gợi ý lời giải
Bài 1: tìm sản phẩm có giá không quá 250.000 VND, sắp xếp giá tăng dần và chỉ trả về sku, name, price_vnd. Nếu bạn đã đổi giá sách theo bài, kết quả cần có sách 220.000 và áo 250.000.
db.products.find(
{ price_vnd: { $lte: 250000 } },
{ _id: 0, sku: 1, name: 1, price_vnd: 1 }
).sort({ price_vnd: 1, sku: 1 })
Bài 2: đưa stock của P001 về 1, chạy cùng lệnh mua một sản phẩm hai lần và quan sát phản hồi. Lần đầu cập nhật một document; lần sau không khớp. Tồn cuối phải là 0.
db.products.updateOne({ _id: "P001" }, { $set: { stock: 1 } })
db.products.updateOne(
{ _id: "P001", stock: { $gte: 1 } },
{ $inc: { stock: -1 } }
)
db.products.updateOne(
{ _id: "P001", stock: { $gte: 1 } },
{ $inc: { stock: -1 } }
)
db.products.findOne({ _id: "P001" }, { _id: 0, stock: 1 })
Bài 3: thêm sản phẩm P004 với mã BOOK-002, sau đó tự viết truy vấn tìm tất cả sách. Nếu bạn đã thực hiện đúng thử nghiệm trùng khóa trước đó, P004 chưa tồn tại nên lệnh dưới sẽ thành công.
db.products.insertOne({
_id: "P004", sku: "BOOK-002", name: "Sách MongoDB thực hành",
category: "book", price_vnd: 180000, stock: 8,
specs: { author: "Tác giả mẫu", pages: 180 }, tags: ["mongodb"]
})
db.products.find({ category: "book" }, { _id: 0, sku: 1, name: 1 })
Kết quả có BOOK-001 và BOOK-002. Hãy tự giải thích vì sao thử thêm trùng sku bị từ chối nhưng thêm sản phẩm này được chấp nhận.
12. Tổng kết và học tiếp
Sau bài học, bạn đã tổ chức một danh mục nhỏ, thực hiện CRUD, đọc thuộc tính lồng nhau, cập nhật tồn kho có điều kiện và kiểm tra mã sản phẩm duy nhất. Kỹ năng cần giữ lại là đọc được ý nghĩa bộ lọc, dự đoán kết quả và đối chiếu với dữ liệu thực tế sau thao tác.
Tiếp theo, hãy kết nối cùng bộ dữ liệu từ Python qua bài làm quen PyMongo. Nếu muốn học thêm về ranh giới giao dịch, tham khảo bài Transaction trong PostgreSQL và đối chiếu theo cơ chế riêng của từng hệ quản trị.
Để kết thúc buổi lab, gõ exit trong mongosh rồi chạy docker stop techdata-mongo-lab ở terminal. Khi học tiếp, dùng docker start techdata-mongo-lab rồi mở lại mongosh; dữ liệu vẫn nằm trong volume đã tạo.
13. Tài liệu tham khảo
- MongoDB Documents: cấu trúc document và BSON.
- MongoDB Data Modeling: thiết kế theo cách truy cập dữ liệu.
- MongoDB CRUD Operations: tổng quan các thao tác dữ liệu.
- db.collection.find(): bộ lọc, projection và cursor.
- db.collection.updateOne(): cập nhật và phản hồi.
- Atomicity and Transactions: phạm vi nguyên tử và transaction.
- db.collection.createIndex(): tạo index và tùy chọn.
Học cùng TechData.AI: nếu bạn muốn xây nền tảng dữ liệu theo lộ trình có hướng dẫn và dự án thực hành, xem các chương trình đào tạo Data & AI để chọn nội dung phù hợp với kiến thức hiện tại.
Comments are closed!