การ update document
การ update คือ operation ที่ผู้คนมักทำผิดแบบแนบเนียนบ่อยที่สุด เพราะแบบจำลองทางความคิดที่ดูเหมือนจะถูก — “ค้นหา document แล้วเขียนทับ field” — ไม่ใช่วิธีที่ MongoDB ทำงานโดยปริยาย การ update คือ document สองตัว นั่นคือ filter ที่เลือกว่าจะเปลี่ยน document ใด และ update document ที่สร้างขึ้นจาก operator ที่อธิบายการเปลี่ยนแปลง operator นั่นแหละคือหัวใจทั้งหมด เพราะทำให้คุณบวกตัวเลข ตั้งค่า field ลบ field หรือ push เข้า array ได้ โดยไม่ไปแตะอย่างอื่นใน document เลย
เราจะแก้ไข members ห้องสมุดของเราต่อไป ทุกตัวอย่างด้านล่างแสดง document ทั้ง ก่อน และ หลัง เพื่อให้คุณเห็นได้ชัดเจนว่าแต่ละ operator ทำอะไรไป
updateOne กับ $set
หัวข้อที่มีชื่อว่า “updateOne กับ $set”$set เขียนค่าลงไปใน field ถ้า field มีอยู่แล้วก็เขียนทับ ถ้ายังไม่มีก็สร้างให้ใหม่ updateOne ใช้การเปลี่ยนแปลงกับ document แรก ที่ตรงกับ filter แล้วหยุดที่นั่น ในที่นี้เราบันทึกว่า Edsger จ่ายค่าปรับครบแล้ว:
db.members.updateOne( { name: "Edsger" }, { $set: { fines: 0 } })const res = await db.collection("members").updateOne( { name: "Edsger" }, { $set: { fines: 0 } });console.log(res.matchedCount, res.modifiedCount);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);ก่อนการ update Edsger ติดหนี้อยู่ห้า:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d3", "name": "Edsger", "joined": 2022, "fines": 5 }หลังจากนั้น เปลี่ยนแปลงเฉพาะ field fines เท่านั้น:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d3", "name": "Edsger", "joined": 2022, "fines": 0 }acknowledgement บอกคุณว่า operation เป็นอย่างไร matchedCount คือจำนวน document ที่ filter พบ ส่วน modifiedCount คือจำนวนที่เปลี่ยนแปลงไปจริง ๆ:
{ "acknowledged": true, "matchedCount": 1, "modifiedCount": 1 }ถ้าคุณรันการ update เดียวกันเป๊ะ ๆ อีกครั้ง ค่าก็เป็น 0 อยู่แล้ว ดังนั้น matchedCount ยังคงเป็น 1 แต่ modifiedCount เป็น 0 — document ตรงกันแต่ไม่มีอะไรที่ต้องเปลี่ยน
$inc และการเพิ่ม field ใหม่
หัวข้อที่มีชื่อว่า “$inc และการเพิ่ม field ใหม่”$inc บวกตัวเลขเข้าไปใน field (ใส่เลขติดลบเพื่อหักออก) และเป็น atomic จึงเป็นเครื่องมือที่ถูกต้องสำหรับตัวนับและยอดคงเหลือ $set ยังทำหน้าที่เป็นวิธี เพิ่ม field ที่ไม่เคยมีอยู่อีกด้วย — ในที่นี้เราทั้งคิดค่าปรับล่าช้าและประทับปี lastSeen ให้สมาชิกในการเรียกครั้งเดียว:
db.members.updateOne( { name: "Linus" }, { $inc: { fines: 3 }, $set: { lastSeen: 2026 } })await db.collection("members").updateOne( { name: "Linus" }, { $inc: { fines: 3 }, $set: { lastSeen: 2026 } });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:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 2 }หลังจากนั้น จำนวนเพิ่มขึ้นสามและ field ใหม่เอี่ยมปรากฏขึ้น:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 5, "lastSeen": 2026}การลบ field ด้วย $unset
หัวข้อที่มีชื่อว่า “การลบ field ด้วย $unset”ในการลบ field ทิ้งไปทั้งหมด — ไม่ใช่กำหนดให้เป็น null แต่ทำให้ field นั้นหายไปเลย — ให้ใช้ $unset ค่าที่ใส่เข้าไปไม่มีผล มีแค่ชื่อ field เท่านั้นที่สำคัญ ในที่นี้เราถอด field lastSeen ออกจาก Linus กลับไป:
db.members.updateOne( { name: "Linus" }, { $unset: { lastSeen: "" } })await db.collection("members").updateOne( { name: "Linus" }, { $unset: { lastSeen: "" } });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 หายไปอีกครั้ง และไม่มีอะไรอย่างอื่นขยับ:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 5 }การทำงานกับ array: $push และ $pull
หัวข้อที่มีชื่อว่า “การทำงานกับ array: $push และ $pull”document มักเก็บ array ไว้ และคุณแทบไม่อยากแทนที่ array ทั้งก้อนเพื่อเพิ่มหรือลบ element เพียงตัวเดียว $push ต่อท้าย element หนึ่งตัว ส่วน $pull ลบทุก element ที่ตรงกับค่าหนึ่ง ลองนึกภาพว่าสมาชิกแต่ละคนถือ array borrowed ของชื่อหนังสือ ก่อนอื่นเราให้ Margaret ยืมหนังสือเล่มหนึ่ง:
db.members.updateOne( { name: "Margaret" }, { $push: { borrowed: "Compilers" } })await db.collection("members").updateOne( { name: "Margaret" }, { $push: { borrowed: "Compilers" } });db.members.update_one( {"name": "Margaret"}, {"$push": {"borrowed": "Compilers"}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Margaret"}, bson.M{"$push": bson.M{"borrowed": "Compilers"}},)if err != nil { return err}coll.update_one( doc! { "name": "Margaret" }, doc! { "$push": { "borrowed": "Compilers" } },).await?;ถ้า field borrowed ยังไม่มีอยู่ $push จะสร้างให้ใหม่เป็น array ที่มี element เดียว:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d2", "name": "Margaret", "joined": 2023, "fines": 0, "borrowed": ["Compilers"]}เมื่อคืนหนังสือแล้ว $pull จะเอาออกจาก array ตามค่าที่ระบุ:
db.members.updateOne( { name: "Margaret" }, { $pull: { borrowed: "Compilers" } })await db.collection("members").updateOne( { name: "Margaret" }, { $pull: { borrowed: "Compilers" } });db.members.update_one( {"name": "Margaret"}, {"$pull": {"borrowed": "Compilers"}},)_, 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! { "$pull": { "borrowed": "Compilers" } },).await?;element หายไปและ array ว่างเปล่าอีกครั้ง:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d2", "name": "Margaret", "joined": 2023, "fines": 0, "borrowed": []}updateMany: เปลี่ยนทุกรายการที่ตรงกัน
หัวข้อที่มีชื่อว่า “updateMany: เปลี่ยนทุกรายการที่ตรงกัน”updateMany คือการเรียกเดียวกันกับ updateOne แต่จะเปลี่ยน document ทุก รายการที่ filter เลือกไว้ ในที่นี้เรายกค่าปรับให้หนึ่งครั้งสำหรับทุกคนที่ติดหนี้อยู่ในตอนนี้ — สังเกต $inc ที่เป็นค่าติดลบ:
db.members.updateMany( { fines: { $gt: 0 } }, { $inc: { fines: -1 } })const res = await db.collection("members").updateMany( { fines: { $gt: 0 } }, { $inc: { fines: -1 } });console.log(res.matchedCount, res.modifiedCount);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 เดียวนี้:
{ "acknowledged": true, "matchedCount": 2, "modifiedCount": 2 }ใน Compass: ดับเบิลคลิกค่าของ field ใด ๆ ในแท็บ Documents เพื่อแก้ค่าแบบ inline ได้เลย หรือใช้ปุ่ม Update หลังจากป้อน filter เพื่อใช้ update document กับหลายรายการที่ตรงกันในคราวเดียว
เคล็ดลับและจุดที่ต้องระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและจุดที่ต้องระวัง”- จุดที่ต้องระวังที่สำคัญที่สุด: ถ้าคุณส่ง update document ที่ ไม่มี operator เลย — document ธรรมดาแบบที่คุณจะใช้ insert — MongoDB จะถือว่าเป็น การแทนที่ทั้งก้อน (full replacement) และล้างทุก field ที่คุณไม่ได้ใส่ไว้ทิ้ง เกือบจะเสมอที่คุณต้องการ
$setไม่ใช่การแทนที่เปล่า ๆ matchedCountและmodifiedCountสามารถต่างกันได้ document สามารถตรงกับ filter แต่ไม่ถูกแก้ไข เพราะค่าใหม่เท่ากับค่าเก่า การรัน update ที่เป็น idempotent ซ้ำคือกรณีคลาสสิกupdateManyไม่มีลิมิตในตัว จะเปลี่ยน ทุก รายการที่ match ตรวจ filter ของคุณให้ดีก่อนรัน filter ที่กว้างเกินไปจะเขียนทับ document มากกว่าที่คุณตั้งใจไว้อย่างเงียบ ๆ$setสร้าง field ที่ขาดหายไป$unsetลบ field ทิ้ง ส่วน$incทำงานได้แม้ field ยังไม่มี (เริ่มนับจากศูนย์) operator เหล่านี้ให้อภัยในเรื่องว่า field มีอยู่แล้วหรือยัง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
updateOne | กระทบ document เดียว เจาะจง เข้าใจง่ายว่าอะไรถูกแก้ | ต้องเรียกซ้ำถ้าต้องแก้หลาย document ทำให้เดินทางไปกลับหลายครั้ง |
updateMany | แก้หลาย document ในการเรียกเดียว เร็วกว่ามากเมื่อ scope ใหญ่ | atomic เฉพาะ “ต่อ document” เท่านั้น ไม่ atomic ข้าม document — document อื่นอาจอ่านเห็นสถานะที่แก้ไปแล้วบางส่วนระหว่างที่ operation ยังไม่จบ |
update ด้วย operator ($set บางส่วน) | แก้เฉพาะ field ที่ต้องการ ส่วนที่เหลือของ document ปลอดภัย | ต้องจำเสมอว่าใส่ operator ทุกครั้ง ไม่งั้นเสี่ยงโดนแทนที่ทั้งก้อน |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ลืม await ผลลัพธ์ของ
updateOne/updateManyแล้วไปทำ logic ถัดไปทันที — โค้ดเดินหน้าต่อโดยสมมติว่า update สำเร็จแล้ว ทั้งที่ยังไม่รู้matchedCount/modifiedCountเลย ทำให้ debug ยากเมื่อ update จริง ๆ ไม่ได้ผล - ใช้
updateOneวนลูปเมื่อจริง ๆ ต้องการupdateMany— ต้องการแก้ document หลายรายการที่ตรง filter เดียวกัน แต่ดัน loop เรียกupdateOneทีละตัว ทั้งที่updateManyทำงานเดียวกันในการเรียกครั้งเดียวและเร็วกว่ามาก - เข้าใจว่า
updateManyเป็น transaction เดียวที่ atomic ทั้งชุด — ความจริงคือ atomic แค่ระดับ document แต่ละอันเท่านั้น ถ้าต้องการความ atomic ข้าม document จริง ๆ ต้องใช้ multi-document transaction ไม่ใช่แค่updateMany
💡 ตัวอย่างจากของจริง
ระบบตัดสต็อกสินค้าตอนสั่งซื้อ — เมื่อ order ถูกยืนยัน ระบบเรียก
updateOneเพิ่ม fieldstatusของ order เป็น"confirmed"ด้วย$setแล้วเรียกupdateMany($inc: { stock: -qty }) แยกกับสินค้าหลายรายการในตะกร้าเดียว โดยรู้ตัวว่าทั้งสอง operation ไม่ atomic ร่วมกัน จึงห่อด้วย transaction เมื่อความถูกต้องสำคัญมากระบบแจ้งเตือนแบบ batch — ทำ flash sale แล้วต้องการติด flag
notified: trueให้ผู้ใช้ที่สมัครรับข่าวหมวดนั้นทุกคนพร้อมกัน ใช้updateMany({ subscribedCategory: "electronics" }, { $set: { notified: true } })ครั้งเดียวแทนการวนลูป update ทีละคน