การอัปเดต document
การอัปเดตคือ operation ที่คนมักทำผิดแบบแนบเนียนที่สุด เพราะภาพในหัวที่ดูเป็นธรรมชาติ — “หา document แล้วเขียนทับ field” — ไม่ใช่วิธีที่ MongoDB ทำงาน การอัปเดตประกอบด้วยสอง document: filter ที่เลือก document ที่จะเปลี่ยน และ update document ที่สร้างจาก operator ซึ่งอธิบายการเปลี่ยนแปลง operator คือหัวใจทั้งหมด มันให้คุณบวกเลข ตั้งค่า field ลบ field หรือ push เข้า array โดยไม่แตะส่วนอื่นของ document ส่วนการแทนที่ document ทั้งก้อนใช้เมธอด อีกตัว คือ replaceOne และ driver จะไม่ยอมให้คุณสับสนสองอย่างนี้ (พิสูจน์ด้านล่าง)
เรายังคงแก้ไข members ของห้องสมุดต่อไป ทุก output ด้านล่างจับมาจาก mongo 8.3.11, mongosh 2.11.1 และ Node driver 7.7.0 (แท็บ Python, Go และ Rust รันหนึ่งครั้งในโปรแกรม scratch บน server เดียวกัน) id บนเครื่องของคุณจะต่างออกไป
ข้อมูลที่เราอัปเดต
หัวข้อที่มีชื่อว่า “ข้อมูลที่เราอัปเดต”แต่ละบทเรียน seed ข้อมูลของตัวเอง:
db.members.insertMany([ { name: "Edsger", joined: 2022, fines: 5 }, { name: "Linus", joined: 2023, fines: 2 }, { name: "Margaret", joined: 2023, fines: 0 }, { name: "Grace", joined: 2024, fines: 0, city: "Arlington", borrowed: ["Compilers"] },]);assert.strictEqual(db.members.countDocuments(), 4);await db.collection("members").insertMany([ { name: "Edsger", joined: 2022, fines: 5 }, { name: "Linus", joined: 2023, fines: 2 }, { name: "Margaret", joined: 2023, fines: 0 }, { name: "Grace", joined: 2024, fines: 0, city: "Arlington", borrowed: ["Compilers"] },]);if ((await db.collection("members").countDocuments()) !== 4) throw new Error("expected 4");updateOne กับ $set
หัวข้อที่มีชื่อว่า “updateOne กับ $set”$set เขียนค่าลงใน field ถ้า field มีอยู่จะถูกเขียนทับ ถ้าไม่มีจะถูกสร้าง updateOne ใช้การเปลี่ยนแปลงกับ document แรก ที่ตรงกับ filter แล้วหยุด ที่นี่เราบันทึกว่า Edsger จ่ายค่าปรับครบแล้ว:
const res = db.members.updateOne( { name: "Edsger" }, { $set: { fines: 0 } });console.log(res);console.log(db.members.updateOne({ name: "Edsger" }, { $set: { fines: 0 } })); // the very same update againconsole.log(db.members.updateOne({ name: "Nobody" }, { $set: { fines: 0 } })); // no matchassert.strictEqual(res.modifiedCount, 1);const res = await db.collection("members").updateOne( { name: "Edsger" }, { $set: { fines: 0 } });console.log(res);console.log(res.matchedCount, res.modifiedCount);if (res.modifiedCount !== 1) throw new Error("expected 1 modified");res = db.members.update_one( {"name": "Edsger"}, {"$set": {"fines": 0}},)print(res.matched_count, res.modified_count)res, err := coll.UpdateOne( ctx, bson.M{"name": "Edsger"}, bson.M{"$set": bson.M{"fines": 0}},)if err != nil { return err}fmt.Println(res.MatchedCount, res.ModifiedCount)let res = coll .update_one( doc! { "name": "Edsger" }, doc! { "$set": { "fines": 0 } }, ) .await?;println!("{} {}", res.matched_count, res.modified_count);ก่อนอัปเดต Edsger ค้างห้า ({ name: 'Edsger', joined: 2022, fines: 5 }); หลังอัปเดตเปลี่ยนเฉพาะ fines (fines: 0) acknowledgement บอกว่า operation เป็นอย่างไร matchedCount คือจำนวน document ที่ filter พบ; modifiedCount คือจำนวนที่เปลี่ยนจริง สามการเรียกใน shell ด้านบนพิมพ์ (รูปร่าง ack เหมือนกันทั้งสามครั้ง ต่างที่ตัวเลข):
{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 } <- first run{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 0, upsertedCount: 0 } <- same update again{ acknowledged: true, insertedId: null, matchedCount: 0, modifiedCount: 0, upsertedCount: 0 } <- no such memberใน Node driver ผลลัพธ์คือ { acknowledged: true, modifiedCount: 1, upsertedId: null, upsertedCount: 0, matchedCount: 1 } และแท็บอื่นเปิดเผยสองตัวเลขเดียวกัน (matched_count, MatchedCount, matched_count) สังเกตบรรทัดที่สอง: รันอัปเดตเดิมซ้ำ matchedCount ยังเป็น 1 แต่ modifiedCount เป็น 0 — document ตรงแต่ไม่มีอะไรต้องเปลี่ยน บรรทัดที่สาม: filter ที่ไม่ตรงอะไรเลย ไม่ใช่ error แค่ matchedCount: 0 ส่วน insertedId: null ใน shell เป็นฟิลด์ตกค้างของ ack การอัปเดต ไม่ต้องสนใจ
$inc และการเพิ่ม field ใหม่
หัวข้อที่มีชื่อว่า “$inc และการเพิ่ม field ใหม่”$inc บวกตัวเลขเข้ากับ field (ใช้เลขลบเพื่อลบ) มันเป็น atomic ซึ่งเป็นเครื่องมือที่ถูกต้องสำหรับตัวนับและยอดเงิน $set ยังใช้ เพิ่ม field ที่ไม่เคยมีได้ — ที่นี่เราทั้งคิดค่าปรับล่าช้าและประทับปี lastSeen ให้สมาชิกในการเรียกเดียว:
db.members.updateOne( { name: "Linus" }, { $inc: { fines: 3 }, $set: { lastSeen: 2026 } });console.log(db.members.findOne({ name: "Linus" }, { _id: 0 }));assert.strictEqual(db.members.findOne({ name: "Linus" }).fines, 5);await db.collection("members").updateOne( { name: "Linus" }, { $inc: { fines: 3 }, $set: { lastSeen: 2026 } });const linus = await db.collection("members").findOne({ name: "Linus" }, { projection: { _id: 0 } });console.log(linus);if (linus.fines !== 5 || linus.lastSeen !== 2026) throw new Error("unexpected Linus");db.members.update_one( {"name": "Linus"}, {"$inc": {"fines": 3}, "$set": {"lastSeen": 2026}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Linus"}, bson.M{ "$inc": bson.M{"fines": 3}, "$set": bson.M{"lastSeen": 2026}, },)if err != nil { return err}coll.update_one( doc! { "name": "Linus" }, doc! { "$inc": { "fines": 3 }, "$set": { "lastSeen": 2026 } },).await?;ก่อนหน้านี้ Linus มีค่าปรับสองและไม่มี field lastSeen; หลังจากนั้นค่าปรับเพิ่มขึ้นสาม และมี field ใหม่ปรากฏ:
{ name: 'Linus', joined: 2023, fines: 5, lastSeen: 2026 }พฤติกรรมสองข้อที่ควรรู้ พิสูจน์ทั้งคู่: $inc บน field ที่ไม่มีเริ่มจากศูนย์ ({ _id: 1 } -> { _id: 1, visits: 1 }) และ $inc บน field ที่ไม่ใช่ตัวเลขล้มเหลวเสียงดัง:
MongoServerError: Plan executor error during update :: caused by :: Cannot apply $inc to a value of non-numeric type. {_id: ObjectId('...')} has the field 'name' of non-numeric type stringลบ field ด้วย $unset
หัวข้อที่มีชื่อว่า “ลบ field ด้วย $unset”การลบ field ทั้งหมด — ไม่ใช่ตั้งเป็น null แต่ทำให้มันไม่มีอยู่ — ใช้ $unset ค่าที่ให้ถูกเมิน สำคัญแค่ชื่อ field ที่นี่เราถอด field lastSeen ออกจาก Linus:
db.members.updateOne({ name: "Linus" }, { $set: { lastSeen: 2026 } });db.members.updateOne( { name: "Linus" }, { $unset: { lastSeen: "" } });console.log(db.members.findOne({ name: "Linus" }, { _id: 0 }));assert.strictEqual("lastSeen" in db.members.findOne({ name: "Linus" }), false);await db.collection("members").updateOne( { name: "Linus" }, { $unset: { lastSeen: "" } });const linus = await db.collection("members").findOne({ name: "Linus" }, { projection: { _id: 0 } });console.log(linus);if ("lastSeen" in linus) throw new Error("lastSeen should be gone");db.members.update_one( {"name": "Linus"}, {"$unset": {"lastSeen": ""}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Linus"}, bson.M{"$unset": bson.M{"lastSeen": ""}},)if err != nil { return err}coll.update_one( doc! { "name": "Linus" }, doc! { "$unset": { "lastSeen": "" } },).await?;field lastSeen หายไปอีกครั้ง และไม่มีอะไรอื่นขยับ:
{ name: 'Linus', joined: 2023, fines: 5 }ทำงานกับ array: $push และ $pull
หัวข้อที่มีชื่อว่า “ทำงานกับ array: $push และ $pull”document มักมี array และคุณไม่ค่อยอยากแทนที่ทั้ง array เพียงเพื่อเพิ่มหรือลบหนึ่งสมาชิก $push ต่อท้ายสมาชิก; $pull ลบทุกสมาชิกที่ตรงกับค่า สมมติสมาชิกแต่ละคนมี array borrowed ของชื่อหนังสือ ก่อนอื่นเราให้ Margaret ยืมหนังสือ:
db.members.updateOne( { name: "Margaret" }, { $push: { borrowed: "Compilers" } });console.log(db.members.findOne({ name: "Margaret" }, { _id: 0 }));db.members.updateOne( { name: "Margaret" }, { $pull: { borrowed: "Compilers" } });console.log(db.members.findOne({ name: "Margaret" }, { _id: 0 }));assert.deepStrictEqual(Array.from(db.members.findOne({ name: "Margaret" }).borrowed), []);const members = db.collection("members");await members.updateOne( { name: "Margaret" }, { $push: { borrowed: "Compilers" } });console.log((await members.findOne({ name: "Margaret" }, { projection: { _id: 0 } })));await members.updateOne( { name: "Margaret" }, { $pull: { borrowed: "Compilers" } });const m = await members.findOne({ name: "Margaret" }, { projection: { _id: 0 } });console.log(m);if (m.borrowed.length !== 0) throw new Error("borrowed should be empty");db.members.update_one( {"name": "Margaret"}, {"$push": {"borrowed": "Compilers"}},)db.members.update_one( {"name": "Margaret"}, {"$pull": {"borrowed": "Compilers"}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Margaret"}, bson.M{"$push": bson.M{"borrowed": "Compilers"}},)if err != nil { return err}_, err = coll.UpdateOne( ctx, bson.M{"name": "Margaret"}, bson.M{"$pull": bson.M{"borrowed": "Compilers"}},)if err != nil { return err}coll.update_one( doc! { "name": "Margaret" }, doc! { "$push": { "borrowed": "Compilers" } },).await?;coll.update_one( doc! { "name": "Margaret" }, doc! { "$pull": { "borrowed": "Compilers" } },).await?;ถ้า field borrowed ยังไม่มี $push จะสร้างเป็น array หนึ่งสมาชิก และ $pull ลบสมาชิกตามค่า ทิ้ง array ว่าง (ไม่ใช่ field ที่หายไป):
{ name: 'Margaret', joined: 2023, fines: 0, borrowed: [ 'Compilers' ] }{ name: 'Margaret', joined: 2023, fines: 0, borrowed: [] }$push บน field ที่ไม่ใช่ array ล้มเหลว: The field 'visits' must be an array but is of type int in document {_id: 1} ญาติสนิทสองตัว: $addToSet ต่อท้ายเฉพาะเมื่อค่ายังไม่มี และ $push กับ $each + $slice เก็บหน้าต่างที่มีขอบเขต — tags: ['a','b'] ที่ push ด้วย { $each: ['c','d','e'], $slice: -3 } กลายเป็น [ 'c', 'd', 'e' ]
updateMany: เปลี่ยนทุกรายการที่ตรง
หัวข้อที่มีชื่อว่า “updateMany: เปลี่ยนทุกรายการที่ตรง”updateMany คือการเรียกเดียวกับ updateOne แต่ใช้การเปลี่ยนแปลงกับ ทุก document ที่ filter เลือก ที่นี่เราลดค่าปรับหนึ่งให้ทุกคนที่ค้างชำระอยู่ — สังเกต $inc ค่าลบ:
const res = db.members.updateMany( { fines: { $gt: 0 } }, { $inc: { fines: -1 } });console.log(res);assert.strictEqual(res.matchedCount, res.modifiedCount);const res = await db.collection("members").updateMany( { fines: { $gt: 0 } }, { $inc: { fines: -1 } });console.log(res.matchedCount, res.modifiedCount);if (res.matchedCount !== res.modifiedCount) throw new Error("expected every match modified");res = db.members.update_many( {"fines": {"$gt": 0}}, {"$inc": {"fines": -1}},)print(res.matched_count, res.modified_count)res, err := coll.UpdateMany( ctx, bson.M{"fines": bson.M{"$gt": 0}}, bson.M{"$inc": bson.M{"fines": -1}},)if err != nil { return err}fmt.Println(res.MatchedCount, res.ModifiedCount)let res = coll .update_many( doc! { "fines": { "$gt": 0 } }, doc! { "$inc": { "fines": -1 } }, ) .await?;println!("{} {}", res.matched_count, res.modified_count);ตัวเลขสะท้อนทุกคนที่ถูกแตะใน operation เดียวนี้ หลังขั้นตอนก่อนหน้า มีเพียง Linus (ค่าปรับ 5) ที่ยังค้างอยู่ ดังนั้น ack คือ matchedCount: 1, modifiedCount: 1 และค่าปรับของเขาลดเหลือ 4 เมื่อมีหลายรายการที่ตรง ทั้งสองตัวเลขคือจำนวน document ที่เปลี่ยน
ใน Compass: เอาเมาส์ชี้ document ใน tab Documents คลิกไอคอนดินสอ แก้ค่าแล้วคลิก Update ตามเอกสาร Compass (modify documents, เรียกอ่านเมื่อ 2026-09-30) มุมมอง List/Table ส่ง findOneAndUpdate (เฉพาะ field ที่คุณเปลี่ยน) ส่วนมุมมอง JSON ส่ง findOneAndReplace (ทั้ง document) — ความต่างระหว่าง update กับ replace เดียวกับด้านล่าง Compass แก้ทีละ document; ถ้าต้องการหลายรายการให้ใช้ updateMany ใน shell ที่ฝังอยู่
แทนที่ทั้ง document: replaceOne
หัวข้อที่มีชื่อว่า “แทนที่ทั้ง document: replaceOne”กับดักใหญ่สำหรับคนที่มาจากระบบอื่น: ถ้าส่ง document ธรรมดาที่ ไม่มี operator ให้ updateOne ในรูปร่างเดียวกับที่ insert ล่ะ? คำตอบตำราคลาสสิก — “MongoDB ถือเป็นการแทนที่ทั้งก้อน” — ไม่ใช่สิ่งที่ driver สมัยใหม่ทำ ทั้ง mongosh 2.11.1 และ Node driver 7.7.0 ปฏิเสธ:
db.people.insertOne({ name: "Grace", joined: 2024, fines: 0, city: "Arlington", borrowed: ["Compilers"] });
// 1. A bare document handed to updateOne: rejected before it reaches the serverlet err;try { db.people.updateOne({ name: "Grace" }, { name: "Grace H.", fines: 9 }); } catch (e) { err = e; }console.log(err.name + ": " + err.message);console.log(db.people.findOne({ name: "Grace" }, { _id: 0 })); // untouched
// 2. replaceOne is the method that replacesconsole.log(db.people.replaceOne({ name: "Grace" }, { name: "Grace H.", fines: 9 }));console.log(db.people.findOne({ name: "Grace H." }));
// 3. ...and the two must not be mixed uplet e2, e3;try { db.people.replaceOne({ name: "Grace H." }, { $set: { a: 1 } }); } catch (e) { e2 = e; }try { db.people.updateOne({ name: "Grace H." }, { $set: { _id: 5 } }); } catch (e) { e3 = e; }console.log(e2.name + ": " + e2.message);console.log(e3.name + ": " + e3.message);assert.strictEqual(err.message, "Update document requires atomic operators");MongoInvalidArgumentError: Update document requires atomic operators{ name: 'Grace', joined: 2024, fines: 0, city: 'Arlington', borrowed: [ 'Compilers' ]}{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0}{ _id: ObjectId('6abca92eacc4516d11798c60'), name: 'Grace H.', fines: 9}MongoInvalidArgumentError: Replacement document must not contain atomic operatorsMongoServerError: Plan executor error during update :: caused by :: Performing an update on the path '_id' would modify the immutable field '_id'ดังนั้น: updateOne/updateMany ต้องการ operator (หรือ aggregation pipeline ด้านล่าง) — document เปล่า ๆ โยน Update document requires atomic operators replaceOne แทนที่ทุกอย่างยกเว้น _id: joined, city และ borrowed หายไป หลังจากนั้น เหลือเพียง name และ fines ซึ่งเป็นเหตุผลที่ไม่ควรหยิบมัน “เพื่อแค่เปลี่ยนชื่อ” _id เปลี่ยนไม่ได้ทั้งสองแบบ ทุก driver ป้องกันแบบเดียวกัน แต่ละตัวด้วยถ้อยคำของตัวเอง — เรารัน update_one/UpdateOne/update_one ที่ส่ง document เปล่า ๆ ในแต่ละภาษา:
Node MongoInvalidArgumentError: Update document requires atomic operatorsPython ValueError: update only works with $ operatorsGo update document must contain key beginning with '$'Rust An invalid argument was provided: update document must only contain update modifiersตัว server เองยังคงถือ document ที่ไม่มี operator เป็นการแทนที่: การส่งคำสั่งดิบ db.runCommand({ update: "people", updates: [{ q: { name: "Grace" }, u: { name: "Grace H." } }] }) คืน n: 1, nModified: 1, ok: 1 และ document ที่เก็บกลายเป็น { name: 'Grace H.' } — joined และ city หายไป ดังนั้น driver และ mongosh คือสิ่งที่ปกป้องคุณ ถ้าคุณพูดโปรโตคอลเองหรือใช้ driver เก่า อาจยังเจอพฤติกรรมแทนที่โดยไม่ตั้งใจแบบเดิม
อัปเดตสมาชิกใน array: positional $, $[], $[id]
หัวข้อที่มีชื่อว่า “อัปเดตสมาชิกใน array: positional $, $[], $[id]”array ของ sub-document คือหัวใจของ schema จริง มีสามรูปแบบ positional เพื่อเข้าถึงสมาชิกโดยไม่ต้องรู้ดัชนี (ข้อมูล: Ada มีสองรายการยืม Grace หนึ่ง):
db.loans.insertMany([ { member: "Ada", books: [{ title: "Compilers", renewed: 0 }, { title: "Algorithms", renewed: 1 }] }, { member: "Grace", books: [{ title: "Compilers", renewed: 2 }] },]);// $ : the first element matched by the FILTERdb.loans.updateOne({ member: "Ada", "books.title": "Compilers" }, { $inc: { "books.$.renewed": 1 } });console.log(db.loans.findOne({ member: "Ada" }, { _id: 0 }).books);// $[id] with arrayFilters: every element matching a conditiondb.loans.updateOne({ member: "Ada" }, { $inc: { "books.$[b].renewed": 10 } }, { arrayFilters: [{ "b.renewed": { $gte: 1 } }] });console.log(db.loans.findOne({ member: "Ada" }, { _id: 0 }).books);// $[] : every elementdb.loans.updateMany({}, { $set: { "books.$[].seen": true } });console.log(db.loans.find({}, { _id: 0 }).toArray().map((d) => d.member + ":" + d.books.every((b) => b.seen)));// $ without an array match in the filter -> errorlet err;try { db.loans.updateOne({ member: "Ada" }, { $inc: { "books.$.renewed": 1 } }); } catch (e) { err = e; }console.log(err.message);assert.match(err.message, /positional operator did not find the match/);[ { title: 'Compilers', renewed: 1 }, { title: 'Algorithms', renewed: 1 } ][ { title: 'Compilers', renewed: 11 }, { title: 'Algorithms', renewed: 11 } ][ 'Ada:true', 'Grace:true' ]Plan executor error during update :: caused by :: The positional operator did not find the match needed from the query.books.$ ต้องการให้ array ปรากฏใน filter ("books.title": "Compilers"); ถ้าไม่มี server ไม่รู้ว่าคุณหมายถึงสมาชิกไหน $[id] + arrayFilters คือรูปแบบทั่วไป; $[] แตะทุกตัว
findOneAndUpdate: เปลี่ยนแล้วอ่านในขั้นเดียว
หัวข้อที่มีชื่อว่า “findOneAndUpdate: เปลี่ยนแล้วอ่านในขั้นเดียว”updateOne คืนจำนวน ไม่ใช่ document เมื่อต้องการ document (คิว หรือตัวนับที่คืนค่าใหม่) ใช้ findOneAndUpdate ซึ่งเป็น atomic — และสังเกตว่าค่าเริ่มต้นคืน document ตามที่เป็น ก่อน อัปเดต:
db.counters.insertOne({ _id: "orders", n: 41 });const before = db.counters.findOneAndUpdate({ _id: "orders" }, { $inc: { n: 1 } });const after = db.counters.findOneAndUpdate({ _id: "orders" }, { $inc: { n: 1 } }, { returnDocument: "after" });const miss = db.counters.findOneAndUpdate({ _id: "nope" }, { $inc: { n: 1 } });console.log(before, after, miss);assert.strictEqual(before.n, 41);assert.strictEqual(after.n, 43);assert.strictEqual(miss, null);{ _id: 'orders', n: 41 } { _id: 'orders', n: 43 } null(Node driver 7.7.0: findOneAndUpdate(filter, update, { returnDocument: "after" }) คืน document เองเลย — { name: 'Grace', joined: 2025 } — และ null เมื่อไม่พบ)
สุดท้าย การอัปเดตยังเป็น aggregation pipeline (array ของ stage) ได้ ซึ่งให้คำนวณ field หนึ่งจากอีก field:
db.loans.insertOne({ member: "Alan", books: [{ renewed: 2 }, { renewed: 3 }] });db.loans.updateOne({ member: "Alan" }, [{ $set: { totalRenewals: { $sum: "$books.renewed" } } }]);console.log(db.loans.findOne({ member: "Alan" }).totalRenewals);assert.strictEqual(db.loans.findOne({ member: "Alan" }).totalRenewals, 5);5เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- กับดัก document เปล่า ฉบับแก้ไข: driver สมัยใหม่ โยน
Update document requires atomic operatorsสำหรับupdateOne(filter, { plain: "document" })— ไม่ได้แทนที่เงียบ ๆ ถ้าคุณ ต้องการ แทนที่ ให้เรียกreplaceOneและจำไว้ว่ามันทิ้งทุก field ที่คุณไม่ได้ใส่ matchedCountและmodifiedCountต่างกันได้ document อาจตรงกับ filter แต่ไม่ถูกแก้ เพราะค่าใหม่เท่าค่าเก่า (matchedCount: 1, modifiedCount: 0ด้านบน)- filter ที่ไม่ตรงอะไรไม่ใช่ error:
matchedCount: 0ตรวจตัวเลขถ้า “ไม่มีอะไรเกิดขึ้น” เป็น bug updateManyไม่มีขีดจำกัดในตัว — เปลี่ยน ทุก รายการที่ตรง ตรวจ filter ให้ดีก่อนรัน$setสร้าง field ที่ขาด,$unsetลบ และ$incใช้ได้แม้ไม่มี field (เริ่มจากศูนย์) แต่ล้มเหลวบน field ที่ไม่ใช่ตัวเลข- สอง operator แตะ path เดียวกันในการอัปเดตเดียวไม่ได้:
{ $set: { fines: 1 }, $inc: { fines: 1 } }ล้มเหลวด้วยUpdating the path 'fines' would create a conflict at 'fines'
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ผู้ช่วยที่ฝึกจากเอกสาร MongoDB เก่ายังอธิบายพฤติกรรมก่อนที่ driver จะมีตัวป้องกัน:
🐛 หา bug ในโค้ดนี้
// AI comment: "updateOne with a plain document sets those fields and keeps the rest"async function renameMember(db, id, newName) {return db.collection("members").updateOne({ _id: id }, { name: newName });}เฉลย
ไม่มีครึ่งไหนของ comment นั้นจริง บน mongosh 2.11.1 และ Node driver 7.7.0 การเรียกนี้ โยน MongoInvalidArgumentError: Update document requires atomic operators — request แรกใน production ตาย “การแก้” ที่ล่อใจคือ replaceOne({ _id: id }, { name: newName }) ซึ่งรันได้ แต่แทนที่ทั้ง document: ในการรันด้านบน joined, city และ borrowed หายไป เหลือเฉพาะชื่อใหม่ (และ _id) วิธีแก้ที่ถูกคือห่อการเปลี่ยนใน $set:
const members = db.collection("members");const { insertedId } = await members.insertOne({ name: "Grace", joined: 2024, fines: 0, city: "Arlington" });
let threw = null;try { await members.updateOne({ _id: insertedId }, { name: "Grace H." }); } catch (e) { threw = e.message; }console.log("bare document:", threw);
await members.updateOne({ _id: insertedId }, { $set: { name: "Grace H." } });const after = await members.findOne({ _id: insertedId }, { projection: { _id: 0 } });console.log("$set :", after);if (threw !== "Update document requires atomic operators") throw new Error("expected the guard error");if (after.name !== "Grace H." || after.city !== "Arlington") throw new Error("$set should keep the other fields");bare document: Update document requires atomic operators$set : { name: 'Grace H.', joined: 2024, fines: 0, city: 'Arlington' }Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”การอัปเดตคือ โปรแกรมสำหรับ document หนึ่งใบ: filter เลือกเป้าหมาย และ operator ($set, $inc, $push, …) คือคำสั่งที่ server รันกับ document นั้น แบบ atomic replaceOne เป็นคำกริยา “สลับทั้งก้อน” เพียงตัวเดียว; อย่างอื่นคือการแก้เฉพาะจุด updateMany รันโปรแกรมนั้นหนึ่งครั้งต่อ document ที่ตรง — แต่ละครั้ง atomic ทั้ง batch ไม่ atomic (reference ของ updateMany บอกไว้ตรง ๆ)
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
updateOne | กระทบ document เดียว แม่นยำและคิดตามได้ง่ายว่าเปลี่ยนอะไร | ต้องเรียกซ้ำหลายครั้งเพื่อเปลี่ยนหลาย document ซึ่งเสียหลาย round trip |
updateMany | เปลี่ยนหลาย document ในการเรียกเดียว เร็วกว่ามากเมื่อสเกล | atomic เฉพาะต่อ document ไม่ใช่ข้าม document — ผู้อ่านอื่นอาจเห็นสถานะที่อัปเดตไปบางส่วนระหว่างที่ทำงาน |
อัปเดตด้วย operator ($set บางส่วน) | เปลี่ยนเฉพาะ field ที่ต้องการ ส่วนที่เหลือปลอดภัย | ต้องใส่ operator เสมอ (driver ปฏิเสธ document เปล่า ๆ) |
replaceOne | สลับทั้ง document ในขั้นเดียว ดีสำหรับการเขียนที่ “document คือแหล่งความจริง” | ทิ้งทุก field ที่คุณละไว้ (joined, city, borrowed หายไปด้านบน) |
findOneAndUpdate | เปลี่ยนและอ่านแบบ atomic ใน round trip เดียว | คืน document เก่า เว้นแต่ returnDocument: "after"; ถือ write lock บน document นั้นเหมือนการอัปเดตทั่วไป |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
updateOne(filter, { plain: 'doc' })แทนที่ document” — มันโยนUpdate document requires atomic operatorsใน mongosh และ driver NodereplaceOneต่างหากที่แทนที่ - “
replaceOneเป็นวิธีปลอดภัยในการเปลี่ยนหนึ่ง field” — มันลบทุก field ที่คุณละ - “อัปเดตที่ไม่เปลี่ยนอะไรคือ error” —
matchedCount: 1, modifiedCount: 0เป็นคำตอบปกติ เช่นเดียวกับmatchedCount: 0 - “
updateManyเป็น transaction atomic เดียว” — atomic เฉพาะการเขียนแต่ละ document; ใช้ multi-document transaction เมื่อต้องการ all-or-nothing - “
findOneAndUpdateคืน document ใหม่” — โดยค่าเริ่มต้นคืนอันเก่า; ขอreturnDocument: "after" - “วน
updateOneกับupdateManyเดียวเหมือนกัน” —updateManyทำใน round trip เดียว
ตัวอย่างจากของจริง
reference ของ
updateManyระบุการรับประกันชัดเจน: “updateMany()modifies each document individually. Each document write is an atomic operation, butupdateMany()as a whole is not atomic” และใช้ภายใน distributed transaction ได้ (db.collection.updateMany, เรียกอ่านเมื่อ 2026-09-30)
แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่พิสูจน์แล้วของ shell/Node; แต่ละแท็บถูกรันหนึ่งครั้งในโปรแกรม scratch กับ server เดียวกันและให้ตัวเลขตามที่แสดง (ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส)