Tổng quan toán tử logic trong MongoDB
Xem thửTình huống mở đầu
Quản lý kho nhắn: "Lấy cho anh các sản phẩm đang bán (status là active) hoặc sắp hết hàng (stock dưới 5)." Bạn viết nhanh find({ status: "active", stock: { $lt: 5 } }) và nhận về đúng 1 sản phẩm. Quản lý nhìn danh sách, thấy thiếu cả monitor lẫn dock, và hỏi lại: "Sao ít vậy?"
Kho có 8 sản phẩm. Hãy đoán trước khi đọc tiếp: nếu viết đúng ý "hoặc", bộ lọc sẽ trả về bao nhiêu sản phẩm? Cuối bài bạn sẽ biết con số, và biết vì sao dấu phẩy trong một object lại đọc là "và".
Ôn nhanh bài trước: { status: { $nin: ["active"] } } có giữ hub, thùng không có field status, không?
Đáp án: có. $nin giữ mọi thùng mà không giá trị nào trong danh sách khớp, kể cả field thiếu. Bài này gom nhiều điều kiện như vậy lại bằng bốn toán tử logic.
Mục tiêu bài học
Sau bài này bạn có thể:
- Kể tên bốn toán tử logic
$and,$or,$nor,$notvà nói mỗi toán tử nhận đối số hình dạng nào. - Nhận ra khi nào một bộ lọc nhiều điều kiện đã là và ngầm, khi nào phải viết toán tử tường minh.
- Phân biệt dạng truy vấn của
findvới dạng biểu thức trong$exprvà$project, kể cả việc dạng biểu thức không có$nor. - Dự đoán kết quả khi điều kiện gặp
null, field thiếu hoặc số 0.
Bức tranh lớn
Xuyên suốt bài, tổ kiểm hàng cầm nhiều phiếu kiểm. Mỗi phiếu là một điều kiện như "status là active". Toán tử logic không kiểm tra thùng, nó chỉ quy định cách đọc các phiếu cùng lúc: phải đúng hết, đúng một cái là đủ, hay phải sai hết. Dự án xuyên suốt vẫn là kho shop với 8 thùng của chương trước.
Hình dưới trả lời một câu hỏi: với hai điều kiện, bạn chọn toán tử nào?
Đang tải sơ đồ…
Cách đọc hình: đọc từ ô xanh dương xuống ô cam đầu tiên. Nếu cả hai điều kiện đều phải đúng, bạn được và (nhánh xanh lá bên trái). Nếu không, ô cam thứ hai hỏi một điều kiện đúng có đủ không: đủ thì là $or, không đủ thì bạn đang muốn loại những thùng khớp, tức $nor hoặc $not (ô đỏ).
Chỗ ẩn dụ không còn đúng: tổ kiểm hàng thật cầm giấy và đọc từng phiếu. MongoDB không bảo đảm thứ tự đọc các phiếu, nó có thể xem phiếu nào có index trước.
Khái niệm cốt lõi
Bài này có bốn khái niệm.
1. Bốn toán tử và hình dạng đối số
| Toán tử | Nhận | Giữ thùng khi | Đứng ở đâu |
|---|---|---|---|
$and | Mảng không rỗng các điều kiện | Mọi điều kiện đúng | Cấp document hoặc lồng trong toán tử logic khác |
$or | Mảng không rỗng các điều kiện | Ít nhất một điều kiện đúng | Cấp document hoặc lồng trong toán tử logic khác |
$nor | Mảng không rỗng các điều kiện | Không điều kiện nào đúng | Cấp document hoặc lồng trong toán tử logic khác |
$not | Một object toán tử hoặc regex | Object đó không khớp | Dưới tên field, ví dụ status: { $not: ... } |
Ba toán tử đầu nhận mảng vì chúng ghép nhiều điều kiện. $not đảo một điều kiện duy nhất nên nó không nhận mảng, và nó không đứng ở cấp document.
2. Và ngầm
Bộ lọc bạn đã viết cả chương trước, như { status: "active", stock: { $gte: 12 } }, thực chất là một phép và: mỗi cặp field với điều kiện là một phiếu, và thùng phải qua mọi phiếu. Cùng một field với nhiều toán tử trong một object, như { stock: { $gt: 5, $lt: 13 } }, cũng là và. Chỉ khi hai phiếu không thể viết thành các khóa khác nhau trong cùng một object thì mới cần $and tường minh, và bài sau sẽ nói kỹ.
3. Hai dạng cú pháp
Trong find, $or: [ { field: điềuKiện }, ... ] là dạng truy vấn: mỗi thùng chỉ có hai trạng thái, khớp hoặc không. Trong $expr, $project hay $addFields, $or: [ biểuThức, ... ] là dạng biểu thức: mỗi phần tử là một biểu thức trả về giá trị, và kết quả là true hoặc false. Dạng biểu thức có $and, $or, $not nhưng không có $nor, và $not ở đó nhận một mảng chứa một biểu thức.
Hình dưới trả lời một câu hỏi: bạn đang ở dạng nào?
Đang tải sơ đồ…
Cách đọc hình: ô xanh dương là câu hỏi duy nhất bạn cần hỏi. Nhánh bên trái là dạng truy vấn, nơi mỗi phần tử của mảng là một bộ lọc nhỏ. Nhánh bên phải là dạng biểu thức, nơi mỗi phần tử là một biểu thức trả về giá trị. Màu chỉ là tín hiệu phụ.
4. Giá trị đúng và sai trong dạng biểu thức
Trong dạng biểu thức, false, null, số 0 và field thiếu đều được đọc là sai. Mọi giá trị khác, kể cả số khác 0, chuỗi và mảng, được đọc là đúng. Điều này khác với dạng truy vấn, nơi field thiếu và null có luật riêng của từng toán tử.
Ví dụ đầu tiên
Trước hết là status và stock của 8 thùng:
db.products.find({}, { name: 1, status: 1, stock: 1 }).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 1, "name" : "keyboard", "stock" : 12, "status" : "active" }
{ "_id" : 2, "name" : "mouse", "stock" : 0, "status" : "active" }
{ "_id" : 3, "name" : "monitor", "stock" : 4, "status" : "archived" }
{ "_id" : 4, "name" : "speaker", "stock" : 15, "status" : "active" }
{ "_id" : 5, "name" : "headset", "stock" : 8, "status" : "active" }
{ "_id" : 6, "name" : "webcam", "stock" : 6, "status" : "draft" }
{ "_id" : 7, "name" : "hub", "stock" : 20 }
{ "_id" : 8, "name" : "dock", "stock" : 3, "status" : null }
Tập A là status active: _id 1, 2, 4, 5. Tập B là stock dưới 5: _id 2, 3, 8. Bộ lọc đầu tiên của bạn trong tình huống mở đầu:
db.products.find(
{ status: "active", stock: { $lt: 5 } }, // hai field trong một object: và ngầm
{ name: 1, status: 1, stock: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
Dự đoán trước khi xem kết quả: những _id nào nằm trong cả A và B?
Kết quả [Đã chạy: MongoDB 4.4.19, mongo shell 4.4.19]:
{ "_id" : 2, "name" : "mouse", "stock" : 0, "status" : "active" }
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
status: "active", stock: { $lt: 5 } | Giữ thùng khi cả hai phiếu đều đúng | Hai khóa trong một object là và ngầm |
{ name: 1, status: 1, stock: 1 } | Chỉ trả ba field cần nhìn | Dễ đối chiếu thùng nào qua phiếu nào |
Chỉ mouse (_id 2) nằm trong cả hai tập. Để có ý "hoặc", đổi sang $or:
db.products.find(
{ $or: [{ status: "active" }, { stock: { $lt: 5 } }] }, // một trong hai điều kiện là đủ
{ name: 1, status: 1, stock: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 1, "name" : "keyboard", "stock" : 12, "status" : "active" }
{ "_id" : 2, "name" : "mouse", "stock" : 0, "status" : "active" }
{ "_id" : 3, "name" : "monitor", "stock" : 4, "status" : "archived" }
{ "_id" : 4, "name" : "speaker", "stock" : 15, "status" : "active" }
{ "_id" : 5, "name" : "headset", "stock" : 8, "status" : "active" }
{ "_id" : 8, "name" : "dock", "stock" : 3, "status" : null }
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
$or: [ | Mở một mảng điều kiện, giữ thùng nếu ít nhất một điều kiện đúng | $or nhận mảng vì nó ghép nhiều điều kiện |
{ status: "active" }, { stock: { $lt: 5 } } | Hai phần tử, mỗi phần tử là một bộ lọc nhỏ đầy đủ | Mỗi phần tử có thể là bất kỳ bộ lọc nào |
Sáu thùng: _id 1, 2, 3, 4, 5, 8. Câu trả lời cho tình huống mở đầu là 6, không phải 1. Bây giờ đến phiên bản ngược lại: thùng không đang bán và cũng không sắp hết.
db.products.find(
{ $nor: [{ status: "active" }, { stock: { $lt: 5 } }] }, // không điều kiện nào đúng
{ name: 1, status: 1, stock: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 6, "name" : "webcam", "stock" : 6, "status" : "draft" }
{ "_id" : 7, "name" : "hub", "stock" : 20 }
Chỉ webcam (draft, stock 6) và hub (không có status, stock 20): đúng hai thùng mà $or ở trên đã bỏ. Hub có mặt dù không có field status, vì phiếu "status là active" sai với nó. Cuối cùng là $not, đứng dưới tên field:
db.products.find(
{ status: { $not: { $eq: "active" } } }, // $not đứng dưới tên field
{ name: 1, status: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 3, "name" : "monitor", "status" : "archived" }
{ "_id" : 6, "name" : "webcam", "status" : "draft" }
{ "_id" : 7, "name" : "hub" }
{ "_id" : 8, "name" : "dock", "status" : null }
Tám thùng trừ bốn thùng active, tức _id 3, 6, 7, 8. Hai thùng thiếu hoặc null (hub, dock) vẫn có mặt, giống $ne ở bài trước, vì $not chỉ đảo kết quả của điều kiện bên trong.
Xem từng bước
Animation dưới đây cho bốn thùng đi qua hai phiếu A và B, rồi xem ba toán tử $and, $or, $nor giữ thùng nào. 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: MongoDB 4.4.19, A là status active, B là stock dưới 5]:
| Thùng | A | B | $and | $or | $nor |
|---|---|---|---|---|---|
| 1 keyboard | Đúng | Sai | Loại | Giữ | Loại |
| 2 mouse | Đúng | Đúng | Giữ | Giữ | Loại |
| 3 monitor | Sai | Đúng | Loại | Giữ | Loại |
| 4 speaker | Đúng | Sai | Loại | Giữ | Loại |
| 5 headset | Đúng | Sai | Loại | Giữ | Loại |
| 6 webcam | Sai | Sai | Loại | Loại | Giữ |
| 7 hub | Sai (thiếu field) | Sai | Loại | Loại | Giữ |
| 8 dock | Sai (null) | Đúng | Loại | Giữ | Loại |
Cột $or và cột $nor luôn trái ngược nhau trên mọi dòng.
Bạn vừa làm được: nhìn hai điều kiện và nói chính xác toán tử nào giữ thùng nào, kể cả thùng thiếu field.
Concept Check
Dùng kho 8 thùng, A là status active, B là stock dưới 5.
Câu 1. { status: "active", stock: { $lt: 5 } } và { $and: [{ status: "active" }, { stock: { $lt: 5 } }] } khác nhau thế nào?
- A. Dạng thứ hai trả thêm các thùng chỉ đúng một điều kiện.
- B. Dạng thứ nhất chỉ nhận một điều kiện, nên bỏ qua stock.
- C. Hai dạng cho cùng kết quả, vì dấu phẩy trong object là và ngầm.
- D. Dạng thứ nhất lỗi vì hai khóa phải đặt trong mảng.
Câu 2. Hub không có field status. $or: [{ status: "active" }, { stock: { $lt: 5 } }] có giữ hub không?
- A. Có, vì thùng thiếu field luôn được
$orgiữ lại. - B. Có, vì stock của hub là 20 nên lớn hơn 5.
- C. Không, vì hub thiếu
statusnên bị loại trước khi xét stock. - D. Không, vì phiếu status sai và phiếu stock cũng sai.
Câu 3. { $nor: [{ status: "active" }, { stock: { $lt: 5 } }] } giữ thùng nào?
- A. Thùng sai cả hai điều kiện, gồm cả thùng thiếu field.
- B. Thùng sai ít nhất một trong hai điều kiện, nên giữ nhiều thùng.
- C. Thùng đúng cả hai điều kiện, tức chỉ mouse.
- D. Thùng có đủ cả hai field status và stock nhưng giá trị khác.
Đáp án và giải thích:
- Câu 1: C. A sai vì
$andvẫn đòi cả hai điều kiện đúng. B sai vì bộ lọc nhiều khóa nào cũng được xét đủ. D sai vì object nhiều khóa là cú pháp hợp lệ, không cần mảng. - Câu 2: D. A sai vì không có quy tắc ưu ái field thiếu ở
$or. B sai vì stock 20 không nhỏ hơn 5. C bịa ra một bước loại thùng trước khi xét stock. Hub bị loại vì cả hai phiếu đều sai. - Câu 3: A. B mô tả
$notbọc$and, không phải$nor. C là kết quả của$and. D bịa ra điều kiện về sự có mặt của field:$norgiữ cả hub.
Làm cùng mình
Quản lý nhắn tiếp: "Cho anh những sản phẩm hết hàng (stock bằng 0) hoặc chưa có giá (price là null) và đừng lấy chúng nữa, anh muốn danh sách các sản phẩm còn lại, có hàng và có giá."
Bước 1. Vẽ ra giấy
Kẻ bảng ba cột stock, price, kết luận cho 8 thùng. Gạch thùng có stock 0 hoặc price null (null và thiếu field đều làm { price: null } đúng). Ghi dự đoán các _id còn lại.
Bước 2. Viết bằng $nor
Điền chỗ trống:
db.products.find(
// TODO 1: giữ thùng mà stock bằng 0 sai và price là null cũng sai
{ name: 1, stock: 1, price: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
Gợi ý: $nor nhận mảng hai điều kiện, mỗi điều kiện là { stock: 0 } và { price: null }. Chạy xong bạn sẽ thấy [Đã chạy] vài dòng _id. So với Bước 1.
Bước 3. Viết lại bằng và ngầm
Cùng ý đó có thể viết không cần $nor. Dự đoán trước: kết quả có giống Bước 2 không?
db.products.find(
// TODO 2: viết lại bằng hai toán tử $ne trong một object
{ name: 1, stock: 1, price: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
Bước 4. Vẽ lại ra giấy
Viết một câu: khi nào $nor gọn hơn hai $ne, và khi nào hai cách này không còn tương đương?
Thử phá nó đi
Năm lỗi người mới hay gặp [Đã chạy: MongoDB 4.4.19]:
| Code lỗi | Kết quả thật | Nguyên nhân | Cách sửa |
|---|---|---|---|
{ status: "active", stock: { $lt: 5 } } mong ý hoặc | Chỉ có _id 2 | Dấu phẩy trong object là và ngầm | Dùng $or, được _id 1, 2, 3, 4, 5, 8 |
{ $or: [] } | code 2: $and/$or/$nor must be a nonempty array | Mảng điều kiện không được rỗng | Kiểm tra độ dài mảng trước khi dựng bộ lọc |
{ $and: { status: "active" } } | code 2: $and must be an array | $and nhận mảng, không nhận object | Bọc điều kiện vào mảng: $and: [ { ... } ] |
{ $not: [{ status: "active" }] } | code 2: unknown top level operator: $not | $not không đứng ở cấp document | Đặt $not dưới field, hoặc dùng $nor |
{ stock: { $not: 5 } } | code 2: $not needs a regex or a document | $not cần object toán tử hoặc regex, không nhận giá trị trần | Viết { stock: { $not: { $eq: 5 } } } |
Bốn thông báo lỗi ở bảng trên là chữ server trả về trong lần chạy thật. Lỗi đầu tiên thì không có thông báo: nó chỉ trả ít thùng hơn bạn nghĩ.
Tự tay làm
Dùng chung kho 8 thùng. Mỗi bài nêu đầu vào, kết quả mong đợi và tiêu chí đạt.
Cơ bản
Bài 1. Điền bảng dự đoán. Ghi các _id được giữ:
| Bộ lọc | Các _id được giữ |
|---|---|
{ $or: [{ stock: 0 }, { price: null }] } | ? |
{ stock: { $ne: 0 }, price: { $ne: null } } | ? |
{ status: "active", price: { $gt: 300000 } } | ? |
Tiêu chí đạt: đúng cả ba dòng. Gợi ý cho dòng ba: speaker có price là chuỗi "350000", và $gt với mốc số có so được chuỗi không?
Vận dụng
Bài 2. Chưa có trạng thái hoặc đã archived. Đầu vào: kho 8 thùng. Viết bộ lọc giữ thùng có status là null (gồm cả thiếu field) hoặc là "archived". Tiêu chí đạt: đúng 3 thùng, gồm monitor, hub và dock. Gợi ý: dùng $or với hai điều kiện, trong đó { status: null } khớp cả null lẫn thiếu.
Thử thách (tùy chọn)
Bài 3. Dạng biểu thức. Viết lại $nor ở Bước 2 của phần Làm cùng mình thành bộ lọc dùng $expr, nhưng cho điều kiện "không active và stock không nhỏ hơn 5". Tiêu chí đạt: đúng 2 thùng là webcam và hub. Gợi ý: dạng biểu thức không có $nor, hãy bọc $or trong $not (nhận mảng một phần tử).
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) Ba toán tử nào nhận mảng, và toán tử nào nhận một object toán tử?
- (Mục tiêu 2, dự đoán)
{ status: "active", stock: { $lt: 5 } }trả về những_idnào, và nó tương đương$andhay$or? - (Mục tiêu 2, giải thích) Vì sao hub xuất hiện trong kết quả
$nordù không có fieldstatus? - (Mục tiêu 3, so sánh) Toán tử nào có ở dạng truy vấn nhưng không có ở dạng biểu thức?
- (Mục tiêu 4, dự đoán) Trong
$project,{ $and: ["$stock"] }cho giá trị gì với mouse (stock 0)?
Điều cần nhớ
- Bốn toán tử logic:
$and,$or,$nornhận mảng không rỗng;$notnhận một object toán tử hoặc regex dưới tên field. - Liệt kê nhiều điều kiện trong một object là và ngầm.
$norgiữ thùng sai mọi điều kiện, nên giữ cả thùng thiếu field.- Dạng truy vấn và dạng biểu thức có luật đúng sai khác nhau, và dạng biểu thức không có
$nor.
Cheat sheet [Đã chạy: mỗi dòng là một lần chạy thật]:
| Muốn | Viết | Các _id được giữ |
|---|---|---|
| Active và stock dưới 5 | { status: "active", stock: { $lt: 5 } } | 2 |
| Active hoặc stock dưới 5 | { $or: [{ status: "active" }, { stock: { $lt: 5 } }] } | 1, 2, 3, 4, 5, 8 |
| Không active và stock không dưới 5 | { $nor: [{ status: "active" }, { stock: { $lt: 5 } }] } | 6, 7 |
| Status không phải active | { status: { $not: { $eq: "active" } } } | 3, 6, 7, 8 |
| Dạng biểu thức: stock trên 10 và active | { $expr: { $and: [{ $gt: ["$stock", 10] }, { $eq: ["$status", "active"] }] } } | 1, 4 |
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 bài sau: nếu viết { stock: { $gt: 10 }, stock: { $lt: 5 } } với hai điều kiện trên cùng một field, thùng nào sẽ được giữ? Và có cách nào viết đúng ý "và" cho trường hợp này không?
Đào sâu
Phần này dành cho bạn tò mò.
- Không có độ ưu tiên giữa và với hoặc. Ngôn ngữ truy vấn của MongoDB không có quy tắc "và trước, hoặc sau" như SQL. Thứ tự do cách bạn lồng điều kiện quyết định. Hai bộ lọc dưới đây dùng cùng ba điều kiện nhưng lồng khác nhau. Chạy trên MongoDB 4.4.19:
db.products.find(
{ status: "active", $or: [{ stock: { $lt: 5 } }, { stock: { $gt: 15 } }] }, // active và (stock nhỏ hoặc lớn)
{ name: 1, status: 1, stock: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 2, "name" : "mouse", "stock" : 0, "status" : "active" }
db.products.find(
{ $or: [{ status: "active", stock: { $lt: 5 } }, { stock: { $gt: 15 } }] }, // (active và stock nhỏ) hoặc stock lớn
{ name: 1, status: 1, stock: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 2, "name" : "mouse", "stock" : 0, "status" : "active" }
{ "_id" : 7, "name" : "hub", "stock" : 20 }
Bộ lọc đầu là active và (stock nhỏ hoặc lớn), chỉ có mouse. Bộ lọc sau là (active và stock nhỏ) hoặc stock lớn, nên có thêm hub (stock 20, dù không active).
- Thứ tự đánh giá và đoản mạch. Tài liệu MongoDB 4.4 của
$andnói rằng nếu một biểu thức sẽ gây lỗi khi chạy riêng thì$andchứa nó có thể gây lỗi nhưng không bảo đảm, và biểu thức đứng sau có thể gây lỗi dù biểu thức đầu sai. Vì vậy đừng dựa vào đoản mạch để tránh lỗi. Quan sát trên 4.4.19 [Đã chạy: chỉ là quan sát, không phải bảo đảm]: trong$project, biểu thức$orbắt đầu bằng phép kiểm stock bằng 0 chạy được với mouse:
db.products.aggregate([
{ $match: { _id: { $in: [1, 2] } } },
{ $project: { _id: 1, ok: { $or: [{ $eq: ["$stock", 0] }, { $gt: [{ $divide: [1, "$stock"] }, 0.05] }] } } },
{ $sort: { _id: 1 } }
]).forEach(printjsononeline)
{ "_id" : 1, "ok" : true }
{ "_id" : 2, "ok" : true }
Đổi chỗ hai biểu thức thì phép chia cho 0 chạy trước:
try {
db.products.aggregate([
{ $match: { _id: { $in: [1, 2] } } },
{ $project: { _id: 1, ok: { $or: [{ $gt: [{ $divide: [1, "$stock"] }, 0.05] }, { $eq: ["$stock", 0] }] } } }
]).toArray()
} catch (e) { print("code " + e.code + ": " + e.message.match(/"errmsg" : "([^"]*)"/)[1]) }
code 16608: can't $divide by zero
Biểu thức hằng như $divide: [1, 0] còn lỗi sớm hơn, ngay lúc tối ưu pipeline, dù đứng sau false:
try {
db.products.aggregate([{ $limit: 1 }, { $project: { _id: 0, ok: { $and: [false, { $divide: [1, 0] }] } } }]).toArray()
} catch (e) { print("code " + e.code + ": " + e.message.match(/"errmsg" : "([^"]*)"/)[1]) }
code 16608: Failed to optimize pipeline :: caused by :: can't $divide by zero
Với dạng truy vấn (find), thứ tự các điều kiện thậm chí còn do query planner quyết định theo index đang có. [Suy luận] theo tài liệu: không nên giả định điều kiện nào được kiểm tra trước.
- Luật đúng sai của dạng biểu thức. Trong
$project, bốn thùng khác nhau cho thấy số 0,nullvà field thiếu đều là sai. Chạy trên MongoDB 4.4.19:
db.products.aggregate([
{ $match: { _id: { $in: [2, 5, 6, 7] } } },
{ $project: { _id: 1, hasStock: { $and: ["$stock"] }, hasPrice: { $and: ["$price"] }, hasStatus: { $and: ["$status"] }, noPrice: { $not: ["$price"] } } },
{ $sort: { _id: 1 } }
]).forEach(printjsononeline)
{ "_id" : 2, "hasStock" : false, "hasPrice" : true, "hasStatus" : true, "noPrice" : false }
{ "_id" : 5, "hasStock" : true, "hasPrice" : false, "hasStatus" : true, "noPrice" : true }
{ "_id" : 6, "hasStock" : true, "hasPrice" : false, "hasStatus" : true, "noPrice" : true }
{ "_id" : 7, "hasStock" : true, "hasPrice" : true, "hasStatus" : false, "noPrice" : false }
Mouse (stock 0) cho hasStock là false dù field có mặt. Headset (price null) và webcam (thiếu price) đều cho hasPrice là false, nên noPrice là true. Hub thiếu status nên hasStatus là false.
- Dạng truy vấn và dạng biểu thức không đồng nhất về null. Chạy trên MongoDB 4.4.19:
db.products.find({ status: null }, { name: 1, status: 1 }).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 7, "name" : "hub" }
{ "_id" : 8, "name" : "dock", "status" : null }
db.products.find({ $expr: { $eq: ["$status", null] } }, { name: 1, status: 1 }).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 8, "name" : "dock", "status" : null }
{ status: null } khớp cả hub (thiếu) lẫn dock (null). Phép $eq trong $expr coi thiếu field khác null, nên chỉ có dock. Khi chuyển một điều kiện từ find sang $expr, hãy kiểm tra lại các thùng thiếu field.
$norkhông có dạng biểu thức. Chạy trên MongoDB 4.4.19:
try {
db.products.find({ $expr: { $nor: [{ $gt: ["$stock", 10] }] } }).toArray()
} catch (e) { print("code " + e.code + ": " + e.message.match(/"errmsg" : "([^"]*)"/)[1]) }
code 168: Unrecognized expression '$nor'
Thông báo cho thấy $nor không phải biểu thức. Muốn có ý tương tự, bọc $or trong $not như Bài 3.
- Giới hạn của bài. Bài này chưa nói đến index cho từng toán tử logic và chưa nói đến
$andvới mảng và$elemMatch. Các bài sau xử lý từng toán tử một. - Đọc thêm. MongoDB Manual 4.4, Logical Query Operators: https://www.mongodb.com/docs/v4.4/reference/operator/query-logical/ và Boolean Aggregation Operators: https://www.mongodb.com/docs/v4.4/reference/operator/aggregation-boolean/
Bài tiếp theo
Bài sau là $and: khi nào dấu phẩy trong một object chưa đủ và bạn bắt buộc phải viết $and tường minh, với khóa trùng tên, hai $or và field mảng. Mini-dự án nối tiếp: viết một bộ lọc cần hai điều kiện trên cùng một field và so sánh kết quả khi viết sai bằng khóa trùng.
Lời giải và đáp án
Làm cùng mình, Bước 2. Điền { $nor: [{ stock: 0 }, { price: null }] },:
db.products.find(
{ $nor: [{ stock: 0 }, { price: null }] },
{ name: 1, stock: 1, price: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 1, "name" : "keyboard", "price" : 450000, "stock" : 12 }
{ "_id" : 3, "name" : "monitor", "price" : 3200000, "stock" : 4 }
{ "_id" : 4, "name" : "speaker", "price" : "350000", "stock" : 15 }
{ "_id" : 7, "name" : "hub", "price" : 180000, "stock" : 20 }
{ "_id" : 8, "name" : "dock", "price" : 450000, "stock" : 3 }
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
$nor: [{ stock: 0 }, { price: null }] | Giữ thùng mà stock bằng 0 sai và price là null cũng sai | Loại mouse (stock 0), headset (null) và webcam (thiếu price) |
Làm cùng mình, Bước 3. Điền { stock: { $ne: 0 }, price: { $ne: null } },:
db.products.find(
{ stock: { $ne: 0 }, price: { $ne: null } }, // viết lại bằng và ngầm
{ name: 1, stock: 1, price: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 1, "name" : "keyboard", "price" : 450000, "stock" : 12 }
{ "_id" : 3, "name" : "monitor", "price" : 3200000, "stock" : 4 }
{ "_id" : 4, "name" : "speaker", "price" : "350000", "stock" : 15 }
{ "_id" : 7, "name" : "hub", "price" : 180000, "stock" : 20 }
{ "_id" : 8, "name" : "dock", "price" : 450000, "stock" : 3 }
Kết quả giống hệt Bước 2: _id 1, 3, 4, 7, 8. Với Bước 4: $nor gọn hơn khi có nhiều điều kiện khác dạng nhau, còn hai $ne tương đương chỉ khi mỗi nhánh của $nor là một phép so bằng trên một field. Khi nhánh là điều kiện phức tạp hơn thì $ne không thay được.
Bài 1.
{ "_id" : 2, "name" : "mouse", "price" : 220000, "stock" : 0 }
{ "_id" : 5, "name" : "headset", "price" : null, "stock" : 8 }
{ "_id" : 6, "name" : "webcam", "stock" : 6 }
{ "_id" : 1, "name" : "keyboard", "price" : 450000, "stock" : 12 }
{ "_id" : 3, "name" : "monitor", "price" : 3200000, "stock" : 4 }
{ "_id" : 4, "name" : "speaker", "price" : "350000", "stock" : 15 }
{ "_id" : 7, "name" : "hub", "price" : 180000, "stock" : 20 }
{ "_id" : 8, "name" : "dock", "price" : 450000, "stock" : 3 }
{ "_id" : 1, "name" : "keyboard", "price" : 450000, "status" : "active" }
Dòng một: mouse, headset, webcam. Dòng hai: mọi thùng còn lại. Dòng ba: chỉ keyboard, vì speaker có giá viết bằng chữ nên $gt với mốc số không so được, và monitor không phải active.
Bài 2.
db.products.find(
{ $or: [{ status: null }, { status: "archived" }] },
{ name: 1, status: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 3, "name" : "monitor", "status" : "archived" }
{ "_id" : 7, "name" : "hub" }
{ "_id" : 8, "name" : "dock", "status" : null }
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
{ status: null } | Khớp thùng có status null hoặc không có field | Hub và dock |
{ status: "archived" } | Khớp monitor | Điều kiện thứ hai của $or |
Bài 3.
db.products.find(
{ $expr: { $not: [{ $or: [{ $eq: ["$status", "active"] }, { $lt: ["$stock", 5] }] }] } },
{ name: 1, status: 1, stock: 1 }
).sort({ _id: 1 }).forEach(printjsononeline)
{ "_id" : 6, "name" : "webcam", "stock" : 6, "status" : "draft" }
{ "_id" : 7, "name" : "hub", "stock" : 20 }
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
$not: [ ... ] | Đảo giá trị của biểu thức bên trong | Dạng biểu thức của $not nhận mảng một phần tử |
$or: [{ $eq: ["$status", "active"] }, { $lt: ["$stock", 5] }] | Đúng khi active hoặc stock dưới 5 | Hub thiếu status nên $eq cho sai, ghép với stock 20 thì $or sai |
Tự kiểm tra.
$and,$or,$nornhận mảng;$notnhận một object toán tử hoặc regex.- Chỉ
_id2. Nó tương đương$and. - Phiếu status active sai với hub (thiếu field) và phiếu stock dưới 5 cũng sai, nên mọi phiếu của
$norsai và hub được giữ. $nor.false, vì số 0 được đọc là sai trong dạng biểu thức.