Upsert trong MongoDB
Xem thửTình huống mở đầu
Anh Bình viết lệnh nhập hàng cho Tạp Hóa Cô Tấm: món có trên kệ thì cộng tồn, chưa có thì tạo thẻ mới. Anh viết hai bước: findOne xem món có chưa, rồi insertOne hoặc updateOne. Sáng thứ Hai hai nhân viên cùng nhập mật ong lúc 8 giờ, và kệ có hai thẻ mật ong. Anh đổi sang một lệnh upsert cho gọn. Tuần sau kệ lại có hai thẻ trà: tea và Tea do anh gõ hoa chữ đầu.
Hai lần trùng thẻ có hai nguyên nhân khác nhau. Hãy đoán trước: upsert giải quyết được chuyện nào trong hai chuyện, và chuyện nào vẫn cần anh tự lo?
Ôn nhanh bài trước: cô Tấm muốn lệnh nhập hàng mà lần đầu thì tạo thẻ món mới, các lần sau chỉ cộng tồn. Có một lệnh nào làm cả hai việc, và nếu hai người cùng nhập một món mới cùng lúc thì sao?
Đáp án: lệnh đó là update với upsert: true. Hai người cùng nhập cùng lúc thì có thể tạo trùng nếu không có chỉ mục duy nhất. Bài này chạy thật cả hai nửa câu trả lời.
Mục tiêu bài học
Sau bài này bạn có thể:
- Dùng
upsert: truevớiupdateOnevà đọc kết quả:matchedCount,modifiedCount,upsertedId. - Dự đoán document mới được dựng từ filter và update ra sao, và dùng
$setOnInsertcho phần chỉ ghi lúc tạo. - Nhận ra bẫy: filter lỏng hoặc gõ lệch hoa thường tạo thẻ mới, lỗi 11000 khi
_idkhớp mà điều kiện khác sai, lỗi 40 và 66 khi toán tử mâu thuẫn. - Dùng chỉ mục duy nhất kèm thử lại bằng update, và dùng
findOneAndUpdateđể sinh số thứ tự.
Bức tranh lớn
Xuyên suốt bài, kệ goods là dãy thẻ hàng và upsert là người nhập hàng mẫu mực: tìm thẻ, có thì ghi thêm, chưa có thì lấy thẻ trống viết mới. Dự án xuyên suốt của chương là cuốn sổ hàng của tiệm: bài này làm lệnh nhập hàng.
Hình dưới trả lời một câu hỏi: lệnh upsert quyết định giữa sửa và tạo thế nào?
Đang tải sơ đồ…
Cách đọc hình: ô xanh dương là lệnh. Ô cam hỏi có document nào khớp filter không. Có thì chạy các toán tử update như bình thường, $setOnInsert bị bỏ qua (ô tím). Không thì server dựng một document mới từ các điều kiện bằng nhau trong filter (ô tím), rồi áp toán tử cùng $setOnInsert, chèn vào và báo upsertedId (ô xanh lá).
Chỗ ẩn dụ không còn đúng: người nhập hàng ngoài đời nhìn kệ rồi mới quyết định. Với upsert, điều kiện khớp chỉ là chữ trong filter của bạn, không phải sự hiểu của người.
Khái niệm cốt lõi
Bài này có ba khái niệm.
1. Sửa khi khớp, chèn khi không khớp
updateOne(filter, update, { upsert: true }) sửa document khớp. Nếu không có, nó chèn một document mới và kết quả có upsertedId, với matchedCount và modifiedCount bằng 0. $setOnInsert chỉ chạy lúc chèn.
2. Document mới được dựng từ filter và update
Các điều kiện bằng nhau trong filter (như name: "gum") được chép vào document mới. Điều kiện so sánh (như price: { $gt: 100 }) không được chép. _id lấy từ filter nếu filter có _id bằng giá trị cụ thể, không thì là ObjectId mới. Các toán tử như $inc và $set được áp lên document mới.
3. Upsert không phải khóa
upsert chỉ không tạo trùng khi filter chỉ vào khóa thật sự duy nhất. Gõ lệch hoa thường tạo thêm thẻ, filter mâu thuẫn gây lỗi, và hai lệnh cùng lúc có thể cùng chèn nếu không có chỉ mục duy nhất. Chỉ mục duy nhất chặn trùng, và bên thua nhận lỗi 11000.
Ví dụ đầu tiên
Nhập hàng bằng upsert cho một món có sẵn và một món mới. Gõ trong mongo shell:
use shop
var a = db.goods.updateOne({ _id: 8 }, { $inc: { stock: 5 } }, { upsert: true })
print("existing:", tojson(a, "", true))
var b = db.goods.updateOne({ _id: 9 }, { $inc: { stock: 5 }, $setOnInsert: { name: "candy" } }, { upsert: true })
print("new:", tojson(b, "", true))
print(tojson(db.goods.findOne({ _id: 9 }), "", true))
Dự đoán trước khi xem kết quả: lệnh thứ nhất cho matchedCount mấy, lệnh thứ hai có upsertedId không, và thẻ món 9 có những field nào?
Kết quả [Đã chạy: MongoDB 4.4.19, mongo shell 4.4.19]:
existing: { "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
new: { "acknowledged" : true, "matchedCount" : 0, "modifiedCount" : 0, "upsertedId" : 9 }
{ "_id" : 9, "name" : "candy", "stock" : 5 }
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
updateOne({ _id: 8 }, { $inc: { stock: 5 } }, { upsert: true }) | Món 8 có sẵn nên cộng 5 vào tồn | Có khớp thì giống một update bình thường |
updateOne({ _id: 9 }, { $inc: ..., $setOnInsert: ... }, { upsert: true }) | Chưa có món 9 nên chèn mới, upsertedId 9, matchedCount 0 | Không khớp thì chèn |
| thẻ món 9 | _id 9, name candy, stock 5 | $inc từ không đến 5, name từ $setOnInsert, _id từ filter |
$setOnInsert chỉ có tác dụng ở lần chèn; ở lệnh chạy lại nó bị bỏ qua, như animation bên dưới cho thấy ở lệnh 3. Document mới được dựng từ filter như thế nào? Thử năm kiểu filter:
function made(filter, update) {
var r = db.goods.updateOne(filter, update, { upsert: true })
return tojson(db.goods.findOne({ _id: r.upsertedId }, { _id: 0 }), "", true)
}
print("equality:", made({ name: "gum", category: "snack" }, { $set: { price: 1000 } }))
print("operator:", made({ name: "mint", price: { $gt: 100 } }, { $set: { stock: 3 } }))
print("one or branch:", made({ $or: [{ name: "orx" }] }, { $set: { stock: 1 } }))
print("two or branches:", made({ $or: [{ name: "ory" }, { name: "orz" }] }, { $set: { stock: 2 } }))
print("set on same field:", made({ name: "conf" }, { $set: { name: "conf2" } }))
Kết quả [Đã chạy: MongoDB 4.4.19]:
equality: { "category" : "snack", "name" : "gum", "price" : 1000 }
operator: { "name" : "mint", "stock" : 3 }
one or branch: { "name" : "orx", "stock" : 1 }
two or branches: { "stock" : 2 }
set on same field: { "name" : "conf2" }
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
{ name: "gum", category: "snack" } | Thẻ mới có cả category và name | Điều kiện bằng nhau được chép vào |
{ name: "mint", price: { $gt: 100 } } | Thẻ mới không có price | Điều kiện so sánh không biết giá trị cụ thể nên không chép |
$or một nhánh và hai nhánh | Một nhánh chép name, hai nhánh thì không | Chỉ khi chắc chắn một giá trị mới chép |
$set: { name: "conf2" } với filter name: "conf" | Thẻ mới có name là conf2 | Toán tử update thắng điều kiện filter |
Xem từng bước
Animation dưới đây chạy năm lệnh upsert. 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]:
| Lệnh | Điều xảy ra | Kệ có |
|---|---|---|
| 1 | _id 8 có sẵn: sửa 1 | 8 món |
| 2 | _id 9 chưa có: tạo, name candy, stock 5 | 9 món |
| 3 | Chạy lại: cộng tồn lên 10, name không đổi | 9 món |
| 4 | name: "Tea" gõ hoa: tạo thêm thẻ | 10 món |
| 5 | _id 1 kèm name: "wrong": lỗi 11000 | 10 món |
Bạn vừa làm được: dự đoán lệnh nào sửa, lệnh nào tạo, lệnh nào lỗi, và nhận ra chạy lại không nhân đôi thẻ.
Concept Check
Kệ có tea (_id 8, stock 15). Anh Bình chạy updateOne({ name: "tea" }, { $inc: { stock: 10 }, $setOnInsert: { unit: "box" } }, { upsert: true }).
Câu 1. Kết quả là gì?
- A. Tạo thẻ mới tên tea, vì
$setOnInsertcó mặt. - B. Cộng 10 vào tồn, và thẻ tea không có thêm
unitnào. - C. Cộng 10 vào tồn, và thẻ tea thêm
unit: "box"ở lần này. - D. Lỗi, vì
$inckhông đi chung$setOnInsert.
Câu 2. Chạy updateOne({ name: "Tea" }, { $inc: { stock: 10 } }, { upsert: true }) trên cùng kệ. Điều gì xảy ra?
- A. Cộng tồn cho thẻ tea, vì hoa thường không quan trọng.
- B. Lỗi 11000, vì tên Tea trùng với tea.
- C. Tạo thêm thẻ Tea, vì
Teakhácteakhi so khớp. - D. Không làm gì vì không có thẻ Tea.
Câu 3. updateOne({ _id: 1, name: "wrong" }, { $inc: { stock: 1 } }, { upsert: true }) khi _id 1 đã là gạo. Kết quả?
- A. Cộng tồn cho gạo, vì
_id1 khớp. - B. Tạo thẻ mới với
_idngẫu nhiên. - C. Lỗi 11000, vì không khớp cả hai điều kiện nên server thử chèn
_id1 đã có. - D. Không làm gì, vì filter mâu thuẫn bị bỏ qua.
Đáp án và giải thích:
- Câu 1: B. Có khớp nên
$setOnInsertbị bỏ qua. A, C, D sai cơ chế. - Câu 2: C. So khớp chuỗi phân biệt hoa thường. A sai, B sai vì không có chỉ mục duy nhất trên
name, D sai vì upsert chèn. - Câu 3: C. Không món nào thỏa cả
_idlẫn tên, server chèn_id1 và va vào gạo. A, B, D bịa ra kết quả.
Làm cùng mình
Cô Tấm muốn một hàm nhập hàng: món có rồi thì cộng tồn; món chưa có thì tạo thẻ với category là new và unit là piece. Thử với tea hai lần và pepper hai lần.
Bước 1. Vẽ ra giấy
Vẽ kệ tám món. Dự đoán lệnh nào tạo thẻ, lệnh nào chỉ cộng, và thẻ pepper cuối cùng có field gì.
Bước 2. Điền chỗ trống
function stockIn(name, qty) {
// TODO 1: upsert theo name, cộng stock bằng qty
// TODO 2: thêm $setOnInsert với category là new và unit là piece
return r.upsertedId === undefined ? "added" : "created"
}
print(stockIn("tea", 10))
print(stockIn("pepper", 4))
// TODO 3: nhập pepper thêm 6 rồi in thẻ pepper bằng findOne
Gợi ý chỗ trống thứ nhất và thứ hai: var r = db.goods.updateOne({ name: name }, { $inc: { stock: qty }, $setOnInsert: { category: "new", unit: "piece" } }, { upsert: true }). Chỗ trống thứ ba: stockIn("pepper", 6) rồi findOne({ name: "pepper" }, { _id: 0 }).
Bước 3. So kết quả
Chạy xong, đối chiếu ở mục Lời giải và đáp án.
Bước 4. Hỏi thêm
Nếu lần nhập thứ hai gõ nhầm Pepper, kệ có mấy thẻ pepper, và bạn đổi gì để tránh?
Thử phá nó đi
Hình dưới trả lời một câu hỏi: nếu hai lệnh upsert cùng khóa chạy cùng lúc thì chuyện gì xảy ra?
Đang tải sơ đồ…
Cách đọc hình: ô cyan là hai nơi cùng gửi upsert cho một khóa mới. Ô cam hỏi có chỉ mục duy nhất trên khóa không. Không có thì cả hai có thể cùng chèn và tạo thẻ trùng (ô đỏ). Có thì chỉ một lệnh chèn thành công, bên kia nhận lỗi 11000 hoặc được server thử lại (ô tím), và bên thua nên lặp lại thành một update thường (ô xanh lá). Mình thử sáu shell chạy song song, mỗi shell upsert 300 khóa: không có chỉ mục duy nhất thì trong sáu lần có một lần một khóa bị chèn hai lần; có chỉ mục duy nhất thì bốn lần không thấy lỗi nào. Kết quả không lặp lại được nên bài không nhúng nó [Có thể khác nhau].
Năm cách làm hỏng chạy được trên máy một mình:
function show(label, f) {
try { var r = f(); print(label + ": " + (r.upsertedId === undefined ? "matched " + r.matchedCount : "created")) }
catch (e) { print(label + ": " + (e.code === undefined ? "shell, no code" : "code " + e.code + " " + e.message.split(":")[0].slice(0, 50))) }
}
show("match by _id", function () { return db.goods.updateOne({ _id: 8 }, { $inc: { stock: 1 } }, { upsert: true }) })
show("_id with a wrong name", function () { return db.goods.updateOne({ _id: 1, name: "wrong" }, { $inc: { stock: 1 } }, { upsert: true }) })
show("setOnInsert changes _id", function () { return db.goods.updateOne({ _id: 500 }, { $setOnInsert: { _id: 501 } }, { upsert: true }) })
show("set and setOnInsert", function () { return db.goods.updateOne({ name: "c3" }, { $set: { x: 1 }, $setOnInsert: { x: 2 } }, { upsert: true }) })
show("no operator", function () { return db.goods.updateOne({ name: "q" }, { a: 1 }, { upsert: true }) })
Kết quả [Đã chạy: MongoDB 4.4.19]:
match by _id: matched 1
_id with a wrong name: code 11000 E11000 duplicate key error collection
setOnInsert changes _id: code 66 Performing an update on the path '_id' would modif
set and setOnInsert: code 40 Updating the path 'x' would create a conflict at '
no operator: shell, no code
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
{ _id: 1, name: "wrong" } | Lỗi 11000 trên _id | Không khớp cả hai nên server chèn _id 1 đã có |
$setOnInsert: { _id: 501 } với filter _id: 500 | Lỗi 66 | _id đã lấy từ filter, không đổi được |
$set: { x } cùng $setOnInsert: { x } | Lỗi 40 | Hai toán tử chạm cùng một field |
| update không có toán tử | Shell từ chối, không có mã | Upsert không đổi quy tắc update |
Gõ lệch hoa thường tạo thêm thẻ; collation chữa được:
var strict = db.goods.updateOne({ name: "Tea" }, { $inc: { stock: 10 } }, { upsert: true })
print("exact filter created new:", strict.upsertedId !== undefined)
print("tea documents now:", db.goods.countDocuments({ name: /^tea$/i }))
var loose = db.goods.updateOne({ name: "TEA" }, { $inc: { stock: 10 } }, { upsert: true, collation: { locale: "en", strength: 2 } })
print("case-insensitive matched:", loose.matchedCount, "created new:", loose.upsertedId !== undefined)
print("tea documents now:", db.goods.countDocuments({ name: /^tea$/i }))
Kết quả [Đã chạy: MongoDB 4.4.19]:
exact filter created new: true
tea documents now: 2
case-insensitive matched: 1 created new: false
tea documents now: 2
Với collation: { locale: "en", strength: 2 }, so khớp bỏ qua hoa thường nên lệnh trúng thẻ có sẵn thay vì chèn mới. Nhưng filter theo name vẫn cần chỉ mục duy nhất để chặn hẳn, như bên dưới:
function show(label, f) {
try { var r = f(); print(label + ": " + (r.upsertedId === undefined ? "matched " + r.matchedCount : "created")) }
catch (e) { print(label + ": " + (e.code === undefined ? "shell, no code" : "code " + e.code + " " + e.message.split(":")[0].slice(0, 50))) }
}
var ix = db.goods.createIndex({ name: 1 }, { unique: true })
show("new _id, existing name", function () { return db.goods.updateOne({ _id: 830 }, { $set: { name: "rice" } }, { upsert: true }) })
show("same name, plain update", function () { return db.goods.updateOne({ name: "rice" }, { $inc: { stock: 1 } }, { upsert: true }) })
print("rice documents:", db.goods.countDocuments({ name: "rice" }))
Kết quả [Đã chạy: MongoDB 4.4.19]:
new _id, existing name: code 11000 E11000 duplicate key error collection
same name, plain update: matched 1
rice documents: 1
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
createIndex({ name: 1 }, { unique: true }) | Không cho hai thẻ cùng name | Chặn trùng ở gốc, kể cả khi hai lệnh chạy cùng lúc |
updateOne({ _id: 830 }, { $set: { name: "rice" } }, { upsert: true }) | Lỗi 11000 trên chỉ mục name | Thẻ mới sẽ trùng name với gạo |
updateOne({ name: "rice" }, ...) | Khớp 1, không tạo thêm | Khóa duy nhất thì không bao giờ có hai thẻ |
updateMany cũng có upsert, và hai kết quả cần phân biệt:
var a = db.goods.updateMany({ category: "staple" }, { $set: { checked: true } }, { upsert: true })
print("staple:", a.matchedCount, a.modifiedCount, "inserted:", a.upsertedId !== undefined)
var b = db.goods.updateMany({ category: "nope" }, { $set: { checked: true } }, { upsert: true })
print("nope:", b.matchedCount, "inserted:", b.upsertedId !== undefined, "documents now:", db.goods.countDocuments({}))
Kết quả [Đã chạy: MongoDB 4.4.19]:
staple: 3 3 inserted: false
nope: 0 inserted: true documents now: 9
Có khớp thì updateMany sửa mọi document khớp và không chèn gì. Không khớp thì chèn đúng một document từ filter, kể cả khi bạn tưởng nó chỉ sửa. Khi cần đếm số lần chèn do upsert, dùng bulkWrite:
var r = db.goods.bulkWrite([
{ updateOne: { filter: { _id: 810 }, update: { $set: { a: 1 } }, upsert: true } },
{ updateOne: { filter: { _id: 1 }, update: { $set: { a: 1 } }, upsert: true } }
])
print("bulk matched:", r.matchedCount, "upserted:", r.upsertedCount, "ids:", tojson(r.upsertedIds, "", true))
Kết quả [Đã chạy: MongoDB 4.4.19]:
bulk matched: 1 upserted: 1 ids: { "0" : 810 }
bulkWrite báo matchedCount, upsertedCount và upsertedIds. Kết quả của updateOne thì chỉ có upsertedId. Với mongosh, tên các trường kết quả có thể khác [Suy luận: chưa chạy được vì máy chưa cài mongosh].
Tự tay làm
Dùng chung kệ goods tám món. 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 trace. Đầu vào: kệ tám món. Năm lệnh theo thứ tự: A là upsert _id 8 cộng 5 tồn; B là upsert _id 9 với $inc và $setOnInsert: { name: "candy" }; C là chạy lại lệnh B; D là upsert theo name: "Tea" cộng 10 tồn; E là upsert với { _id: 1, name: "wrong" }. Điền: sửa, tạo hay lỗi, số món trên kệ. Tiêu chí đạt: sửa với 8 món, tạo với 9, sửa với 9 (stock 10), tạo với 10, lỗi 11000.
Vận dụng
Bài 2. Bộ đếm số phiếu. Đầu vào: kệ counters trống. Viết next(name) dùng findOneAndUpdate với $inc, upsert: true và returnNewDocument: true, trả về seq. Gọi receipt, receipt, refund, receipt. Tiêu chí đạt: in 1, 2, 1, 3 và có 2 bộ đếm. Gợi ý: lần đầu bộ đếm chưa có nên $inc bắt đầu từ 0.
Thử thách (tùy chọn)
Bài 3. Chèn, nếu trùng thì cộng. Đầu vào: kệ tám món. Tạo chỉ mục duy nhất trên name. Viết createThenAdd(id, name, qty) thử insertOne, và nếu gặp lỗi 11000 thì updateOne cộng tồn theo name. Gọi với honey hai lần. Tiêu chí đạt: in created, added, tồn 15, đúng 1 thẻ honey. Gợi ý: bắt e.code bằng 11000, lỗi khác thì ném lại.
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) Khi
upsertchèn mới,matchedCount,modifiedCountvàupsertedIdbằng gì? - (Mục tiêu 2, giải thích) Vì sao
$setOnInsertkhông có tác dụng khi chạy lại lệnhupsert? - (Mục tiêu 2, dự đoán) Filter
{ name: "mint", price: { $gt: 100 } }tạo thẻ mới có những field nào? - (Mục tiêu 3, tìm lỗi) Kệ có
teavàTeasau khi nhập hàng bằngupsert. Vì sao, và sửa thế nào? - (Mục tiêu 4, chọn cách) Hai nhân viên cùng nhập món mới. Chỉ mục duy nhất ngăn trùng thế nào, và bên thua nên làm gì?
Điều cần nhớ
upsert: truelà sửa khi khớp, chèn khi không; kết quả chèn cóupsertedId,matchedCountvàmodifiedCountbằng 0.- Document mới gồm điều kiện bằng nhau của filter, toán tử update và
$setOnInsert; điều kiện so sánh bị bỏ;_idlấy từ filter. - Gõ lệch hoa thường tạo thẻ mới; filter mâu thuẫn với
_idgây lỗi 11000; toán tử xung đột gây lỗi 40 hoặc 66. - Chỉ mục duy nhất chặn trùng; bên thua gặp 11000 nên lặp lại thành update;
findOneAndUpdatesinh số thứ tự.
Cheat sheet:
| Muốn | Viết | Kết quả |
|---|---|---|
| Sửa hoặc tạo | updateOne(f, u, { upsert: true }) | upsertedId nếu tạo |
| Chỉ ghi lúc tạo | $setOnInsert: { source: "first" } | Bỏ qua khi khớp |
| Bỏ qua hoa thường | collation: { locale: "en", strength: 2 } | Trúng thẻ có sẵn |
| Chặn trùng | createIndex({ name: 1 }, { unique: true }) | Lỗi 11000 cho bên thua |
| Sinh số thứ tự | findOneAndUpdate(f, { $inc: { seq: 1 } }, { upsert: true, returnNewDocument: true }) | Số kế tiếp |
| Đếm lượt tạo | bulkWrite rồi upsertedCount | Số document được chèn |
Hình tóm tắt của bài là sơ đồ quyết định ở mục Bức tranh lớn.
Câu hỏi để dành cho ngày mai: chương này đã đi từ tạo kệ đến thêm, sửa, xóa, sắp xếp, nối và tìm kiếm. Khi cô Tấm hỏi "làm sao chắc rằng dữ liệu nhập vào đúng dạng ngay từ đầu?", bạn dùng công cụ nào đã gặp ở bài đầu chương?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. Server tìm bằng filter, và nếu không thấy thì dựng document từ các điều kiện bằng nhau cộng toán tử rồi chèn. Giữa hai bước đó không có khóa nào ngăn lệnh khác chen vào, vì vậy chỉ mục duy nhất mới là rào chắn thật. Từ 4.2 server có thể tự thử lại khi gặp 11000 trong một số điều kiện [Suy luận: theo tài liệu, mình không tái hiện được điều này một cách ổn định].
- So sánh với lựa chọn khác.
replaceOnevớiupsertdựng thẻ từ chính thẻ thay thế nên mất field của filter, như bàireplaceOne.findOneAndUpdatevớiupsertcòn trả lại document, tiện cho bộ đếm. - Hiểu lầm nâng cao. Nhiều người nghĩ
upsertan toàn trước chạy đồng thời. Không có chỉ mục duy nhất thì không; có thì bên thua vẫn có thể nhận 11000 và cần code xử lý [Có thể khác nhau: tùy phiên bản và điều kiện]. - Giới hạn của bài. Bài chưa nói về
upserttrong transaction,arrayFiltersvà pipeline update. Cáchmongoshin kết quả là [Suy luận]. - Đọc thêm. MongoDB Manual 4.4, trang
db.collection.updateOne()mục Upsert: https://www.mongodb.com/docs/v4.4/reference/method/db.collection.updateOne/ và trang$setOnInsert: https://www.mongodb.com/docs/v4.4/reference/operator/update/setOnInsert/
Bài tiếp theo
Bài kế tiếp là Kiểm tra Chương 52: tổng hợp toàn bộ chương Document và collection MongoDB gồm tạo kệ, thêm, truy vấn, sửa, xóa, sắp xếp, collection giới hạn, quan hệ, text search, Map-Reduce và upsert. Mini-dự án nối tiếp: ôn lại sổ hàng của Tạp Hóa Cô Tấm trước khi làm bài kiểm tra.
Lời giải và đáp án
Làm cùng mình, Bước 2. Ba chỗ trống đã điền:
function stockIn(name, qty) {
var r = db.goods.updateOne({ name: name }, { $inc: { stock: qty }, $setOnInsert: { category: "new", unit: "piece" } }, { upsert: true })
return r.upsertedId === undefined ? "added" : "created"
}
print(stockIn("tea", 10))
print(stockIn("pepper", 4))
print(stockIn("pepper", 6))
print(tojson(db.goods.findOne({ name: "pepper" }, { _id: 0 }), "", true))
print("tea stock:", db.goods.findOne({ name: "tea" }).stock, "documents:", db.goods.countDocuments({}))
added
created
added
{ "name" : "pepper", "category" : "new", "stock" : 10, "unit" : "piece" }
tea stock: 25 documents: 9
Tea có sẵn nên lệnh đầu cộng tồn (15 lên 25). Pepper chưa có nên lệnh thứ hai tạo thẻ với category là new và unit là piece từ $setOnInsert, stock 4. Lệnh thứ ba cộng thêm 6 nên stock 10 và $setOnInsert không chạy lại. Nếu gõ Pepper, kệ có hai thẻ pepper vì khác hoa thường. Tránh bằng collation strength: 2 hoặc chuẩn hóa tên về chữ thường trước khi ghi.
Bài 1.
function show(label, f) {
try { var r = f(); print(label + ": " + (r.upsertedId === undefined ? "matched " + r.matchedCount : "created")) }
catch (e) { print(label + ": " + (e.code === undefined ? "shell, no code" : "code " + e.code + " " + e.message.split(":")[0].slice(0, 50))) }
}
function line(label, f) { var r = f(); print(label + ": " + (r.upsertedId === undefined ? "matched " + r.matchedCount + ", modified " + r.modifiedCount : "created") + ", documents " + db.goods.countDocuments({})) }
line("A", function () { return db.goods.updateOne({ _id: 8 }, { $inc: { stock: 5 } }, { upsert: true }) })
var up = function () { return db.goods.updateOne({ _id: 9 }, { $inc: { stock: 5 }, $setOnInsert: { name: "candy" } }, { upsert: true }) }
line("B", up)
line("C", up)
print("candy: " + tojson(db.goods.findOne({ _id: 9 }), "", true))
line("D", function () { return db.goods.updateOne({ name: "Tea" }, { $inc: { stock: 10 } }, { upsert: true }) })
show("E", function () { return db.goods.updateOne({ _id: 1, name: "wrong" }, { $inc: { stock: 1 } }, { upsert: true }) })
A: matched 1, modified 1, documents 8
B: created, documents 9
C: matched 1, modified 1, documents 9
candy: { "_id" : 9, "name" : "candy", "stock" : 10 }
D: created, documents 10
E: code 11000 E11000 duplicate key error collection
A sửa với 8 món. B tạo thẻ 9 nên 9 món. C chạy lại nên chỉ sửa, stock lên 10 và name vẫn candy. D tạo thêm thẻ Tea nên 10 món. E lỗi 11000 vì filter mâu thuẫn với gạo.
Bài 2.
function next(name) { return db.counters.findOneAndUpdate({ _id: name }, { $inc: { seq: 1 } }, { upsert: true, returnNewDocument: true }).seq }
print("receipt", next("receipt"))
print("receipt", next("receipt"))
print("refund", next("refund"))
print("receipt", next("receipt"))
print("counters:", db.counters.countDocuments({}))
receipt 1
receipt 2
refund 1
receipt 3
counters: 2
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
findOneAndUpdate({ _id: name }, { $inc: { seq: 1 } }, { upsert: true, returnNewDocument: true }) | Cộng 1 và trả về document sau khi cộng | Bộ đếm chưa có thì tạo, seq từ không lên 1 |
.seq | Lấy số thứ tự mới | Mỗi lần gọi trả số kế tiếp, không trùng |
gọi receipt, receipt, refund, receipt | 1, 2, 1, 3 | Mỗi tên là một bộ đếm riêng |
Bài 3.
var ix = db.goods.createIndex({ name: 1 }, { unique: true })
function createThenAdd(id, name, qty) {
try { db.goods.insertOne({ _id: id, name: name, stock: qty }); return "created" }
catch (e) {
if (e.code !== 11000) { throw e }
db.goods.updateOne({ name: name }, { $inc: { stock: qty } })
return "added"
}
}
print(createThenAdd(900, "honey", 10))
print(createThenAdd(901, "honey", 5))
print("honey stock:", db.goods.findOne({ name: "honey" }).stock, "copies:", db.goods.countDocuments({ name: "honey" }))
created
added
honey stock: 15 copies: 1
Lần đầu insertOne thành công. Lần hai gặp lỗi 11000 vì honey đã có, nên catch chuyển sang updateOne cộng tồn: 15, một thẻ. Đây chính là cách bên thua cuộc xử lý khi hai nơi cùng nhập món mới.
Tự kiểm tra.
matchedCount0,modifiedCount0,upsertedIdlà_idcủa thẻ mới.- Vì khi có khớp thì lệnh là update thường, và
$setOnInsertchỉ áp lúc chèn. - Có
namelàmint, không cópricevì điều kiện so sánh không được chép. - Vì
teavàTealà hai chuỗi khác nhau; dùng collation bỏ qua hoa thường hoặc chuẩn hóa tên trước khi ghi. - Chỉ mục duy nhất chỉ cho một lệnh chèn thành công; bên thua nhận 11000 và lặp lại thành
updateOnecộng tồn.