ข้ามไปยังเนื้อหา

การ update document

การ update คือ operation ที่ผู้คนมักทำผิดแบบแนบเนียนบ่อยที่สุด เพราะแบบจำลองทางความคิดที่ดูเหมือนจะถูก — “ค้นหา document แล้วเขียนทับ field” — ไม่ใช่วิธีที่ MongoDB ทำงานโดยปริยาย การ update คือ document สองตัว นั่นคือ filter ที่เลือกว่าจะเปลี่ยน document ใด และ update document ที่สร้างขึ้นจาก operator ที่อธิบายการเปลี่ยนแปลง operator นั่นแหละคือหัวใจทั้งหมด เพราะทำให้คุณบวกตัวเลข ตั้งค่า field ลบ field หรือ push เข้า array ได้ โดยไม่ไปแตะอย่างอื่นใน document เลย

เราจะแก้ไข members ห้องสมุดของเราต่อไป ทุกตัวอย่างด้านล่างแสดง document ทั้ง ก่อน และ หลัง เพื่อให้คุณเห็นได้ชัดเจนว่าแต่ละ operator ทำอะไรไป

$set เขียนค่าลงไปใน field ถ้า field มีอยู่แล้วก็เขียนทับ ถ้ายังไม่มีก็สร้างให้ใหม่ updateOne ใช้การเปลี่ยนแปลงกับ document แรก ที่ตรงกับ filter แล้วหยุดที่นั่น ในที่นี้เราบันทึกว่า Edsger จ่ายค่าปรับครบแล้ว:

db.members.updateOne(
{ name: "Edsger" },
{ $set: { fines: 0 } }
)

ก่อนการ 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 (ใส่เลขติดลบเพื่อหักออก) และเป็น atomic จึงเป็นเครื่องมือที่ถูกต้องสำหรับตัวนับและยอดคงเหลือ $set ยังทำหน้าที่เป็นวิธี เพิ่ม field ที่ไม่เคยมีอยู่อีกด้วย — ในที่นี้เราทั้งคิดค่าปรับล่าช้าและประทับปี lastSeen ให้สมาชิกในการเรียกครั้งเดียว:

db.members.updateOne(
{ name: "Linus" },
{ $inc: { fines: 3 }, $set: { lastSeen: 2026 } }
)

ก่อนหน้านี้ Linus มีค่าปรับสองและไม่มี field lastSeen:

{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 2 }

หลังจากนั้น จำนวนเพิ่มขึ้นสามและ field ใหม่เอี่ยมปรากฏขึ้น:

{
"_id": "65f0b3d4e4b0a1c2e4b0a1d1",
"name": "Linus",
"joined": 2023,
"fines": 5,
"lastSeen": 2026
}

ในการลบ field ทิ้งไปทั้งหมด — ไม่ใช่กำหนดให้เป็น null แต่ทำให้ field นั้นหายไปเลย — ให้ใช้ $unset ค่าที่ใส่เข้าไปไม่มีผล มีแค่ชื่อ field เท่านั้นที่สำคัญ ในที่นี้เราถอด field lastSeen ออกจาก Linus กลับไป:

db.members.updateOne(
{ name: "Linus" },
{ $unset: { lastSeen: "" } }
)

field lastSeen หายไปอีกครั้ง และไม่มีอะไรอย่างอื่นขยับ:

{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 5 }

document มักเก็บ array ไว้ และคุณแทบไม่อยากแทนที่ array ทั้งก้อนเพื่อเพิ่มหรือลบ element เพียงตัวเดียว $push ต่อท้าย element หนึ่งตัว ส่วน $pull ลบทุก element ที่ตรงกับค่าหนึ่ง ลองนึกภาพว่าสมาชิกแต่ละคนถือ array borrowed ของชื่อหนังสือ ก่อนอื่นเราให้ Margaret ยืมหนังสือเล่มหนึ่ง:

db.members.updateOne(
{ name: "Margaret" },
{ $push: { borrowed: "Compilers" } }
)

ถ้า 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" } }
)

element หายไปและ array ว่างเปล่าอีกครั้ง:

{
"_id": "65f0b3d4e4b0a1c2e4b0a1d2",
"name": "Margaret",
"joined": 2023,
"fines": 0,
"borrowed": []
}

updateMany คือการเรียกเดียวกันกับ updateOne แต่จะเปลี่ยน document ทุก รายการที่ filter เลือกไว้ ในที่นี้เรายกค่าปรับให้หนึ่งครั้งสำหรับทุกคนที่ติดหนี้อยู่ในตอนนี้ — สังเกต $inc ที่เป็นค่าติดลบ:

db.members.updateMany(
{ fines: { $gt: 0 } },
{ $inc: { fines: -1 } }
)

ตอนนี้จำนวนนับสะท้อนทุกคนที่ถูกแตะต้องใน 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 มีอยู่แล้วหรือยัง
ตัวเลือกBenefitCost
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 เพิ่ม field status ของ order เป็น "confirmed" ด้วย $set แล้วเรียก updateMany ($inc: { stock: -qty }) แยกกับสินค้าหลายรายการในตะกร้าเดียว โดยรู้ตัวว่าทั้งสอง operation ไม่ atomic ร่วมกัน จึงห่อด้วย transaction เมื่อความถูกต้องสำคัญมาก

ระบบแจ้งเตือนแบบ batch — ทำ flash sale แล้วต้องการติด flag notified: true ให้ผู้ใช้ที่สมัครรับข่าวหมวดนั้นทุกคนพร้อมกัน ใช้ updateMany({ subscribedCategory: "electronics" }, { $set: { notified: true } }) ครั้งเดียวแทนการวนลูป update ทีละคน

คุณเรียก updateOne ด้วย document ธรรมดาที่ไม่มี operator เลย จะเกิดอะไรขึ้น?
matchedCount เป็น 1 แต่ modifiedCount เป็น 0 หลังการ update เพราะอะไร?
operator ใดลบ field ออกจาก document ทั้งหมด?
คุณต่อท้าย element หนึ่งตัวเข้าไปใน field ที่เป็น array โดยไม่แทนที่ array ได้อย่างไร?