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

CRUD ใน MongoDB

ทุกฐานข้อมูลสรุปลงมาเหลือสี่สิ่งที่คุณทำกับข้อมูลได้ คือ ใส่ระเบียนใหม่เข้าไป อ่านกลับออกมา แก้ไข และลบทิ้ง สี่กริยานี้ย่อรวมกันเป็น CRUD — Create, Read, Update, Delete

ใน MongoDB “ระเบียน” เหล่านั้นคือ document หน้าตาคล้าย JSON ที่อยู่ใน collection และกริยาทั้งสี่จับคู่กับชุด method เล็ก ๆ ที่คาดเดาได้ หน้าตาแทบไม่ต่างกันเลย ไม่ว่าคุณจะพิมพ์ในเชลล์ หรือเรียกผ่าน driver ของ Node.js, Python, Go หรือ Rust

โมดูลนี้คือหัวใจของคอร์ส เมื่อจบโมดูลคุณจะโหลด document เข้า collection ดึงกลับออกมาด้วย filter แก้ field อย่างแม่นยำโดยไม่พังส่วนที่เหลือของ document ลบเฉพาะสิ่งที่ตั้งใจจะลบ และรวมการเขียนหลายครั้งให้เหลือ round trip เดียวได้ ข้อมูลตัวอย่างเราจงใจให้เล็กและเป็นเรื่องสมมติ — สมาชิกห้องสมุดไม่กี่คนกับหนังสือที่ยืมไป — เพื่อให้โฟกัสอยู่ที่ตัว operation

flowchart LR
  App["Your app or mongosh"] -->|"insert / find / update / delete"| Driver["Driver or shell"]
  Driver --> Coll["Collection: members"]
  Coll --> D1["{ _id, name, fines }"]
  Coll --> D2["{ _id, name, fines }"]
  Coll --> D3["{ _id, name, fines }"]
การเรียก CRUD วิ่งจากโค้ดของคุณ ผ่าน driver ไปยัง collection ของ document

แต่ละกลุ่มมี method แบบเอกพจน์ที่แตะ document ได้มากที่สุดหนึ่งรายการ และ method แบบพหูพจน์ที่แตะได้หลายรายการ ทั้งหมดนี้คือคำศัพท์ที่คุณจะได้ใช้ตลอดทั้งโมดูล:

  • Create — insertOne เพิ่ม document ทีละรายการ, insertMany เพิ่มทีละ array
  • Read — findOne คืน document แรกที่ตรงกัน (หรือไม่คืนอะไรเลย), find คืน cursor ครอบทุกรายการที่ตรงกัน
  • Update — updateOne เปลี่ยนรายการแรกที่ตรงกัน, updateMany เปลี่ยนทุกรายการที่ตรงกัน ทั้งคู่ใช้ update operator อย่าง $set และ $inc
  • Delete — deleteOne ลบรายการแรกที่ตรงกัน, deleteMany ลบทุกรายการที่ตรงกัน

รูปแบบเดียวกันนี้ปรากฏในทั้งห้าแท็บที่คุณจะเห็นตลอดโมดูล สังเกตว่าโค้ด insert อ่านแล้วแทบเหมือนกันหมด ทั้งในเชลล์และในทุก driver:

mongosh/crud-insert-tab.js
db.tabs.insertOne({ name: "Ada", fines: 0 })
assert.strictEqual(db.tabs.countDocuments(), 1)

ทุกการเรียก CRUD ตอบด้วย acknowledgement และการอ่านมันคือวิธียืนยันว่าเกิดอะไรขึ้น fence นี้รันหนึ่งการเรียกจากแต่ละกลุ่มใน mongosh และพิมพ์ field ที่ acknowledgement แต่ละอันมี (mongo 8.3.11, mongosh 2.11.1, Mac เครื่องนี้):

mongosh/crud-acks.js
const keys = (o) => Object.keys(o).join(", ");
console.log("insertOne :", keys(db.members.insertOne({ name: "Ada", fines: 0 })));
console.log("insertMany:", keys(db.members.insertMany([{ name: "Grace", fines: 2 }, { name: "Linus", fines: 5 }])));
console.log("updateOne :", keys(db.members.updateOne({ name: "Ada" }, { $set: { fines: 1 } })));
console.log("upsert :", keys(db.members.updateOne({ name: "Nina" }, { $set: { fines: 0 } }, { upsert: true })));
console.log("deleteOne :", keys(db.members.deleteOne({ name: "Nina" })));
console.log("bulkWrite :", keys(db.members.bulkWrite([{ deleteOne: { filter: { name: "Nobody" } } }])));
console.log("find :", db.members.find({ fines: { $gt: 0 } }).toArray().length, "documents");
assert.strictEqual(db.members.countDocuments(), 3);
insertOne : acknowledged, insertedId
insertMany: acknowledged, insertedIds
updateOne : acknowledged, insertedId, matchedCount, modifiedCount, upsertedCount
upsert : acknowledged, insertedId, matchedCount, modifiedCount, upsertedCount
deleteOne : acknowledged, deletedCount
bulkWrite : acknowledged, insertedCount, insertedIds, matchedCount, modifiedCount, deletedCount, upsertedCount, upsertedIds
find : 3 documents

จำ _id ที่ MongoDB เติมให้: insertOne({ name: "Ada", fines: 0 }) เก็บ { _id: ObjectId('...'), name: 'Ada', fines: 0 } (Node driver จัดรูปผลเหล่านี้ต่างออกไปเล็กน้อย — insertedCount บน insertMany, upsertedId บนการอัปเดต — แต่ละบทแสดงทั้งสองแบบ)

ใน Compass: ทั้งสี่กลุ่มจับคู่กับ GUI — tab Documents insert (Add Data > Insert Document), แก้ (ไอคอนดินสอเมื่อชี้ แล้ว Update) และลบ (ไอคอนถังขยะเมื่อชี้); แต่ละบทอ้างเอกสาร Compass ของตัวเอง

บทเรียนถัดไปจะหยิบแต่ละกลุ่มมาทีละกลุ่ม แล้วปิดท้ายด้วยสองท่าไม้ตายที่รวมหลายกลุ่มเข้าด้วยกัน:

  1. Insert — การเพิ่ม document ทีละรายการหรือทีละหลายรายการ และ _id อัตโนมัติ
  2. Read — find เทียบกับ findOne, filter แบบ equality และแบบหลาย field พร้อมแตะ projection เป็นครั้งแรก
  3. Update — $set, $inc, $unset, $push, $pull และการอ่านจำนวนที่ตรงกัน (match count) ที่การเขียนคืนกลับมา
  4. Delete — การลบ document อย่างแม่นยำ บวกกับรูปแบบ soft-delete
  5. Upsert และ bulk write — “insert ถ้ายังไม่มี ไม่อย่างนั้นก็ update” และการยัดหลาย operation ให้เหลือ round trip เดียวไป server
  • method แบบเอกพจน์ (insertOne, findOne, updateOne, deleteOne) ไม่มีทางกระทบ document เกินหนึ่งรายการ แม้จะมีหลายรายการตรงกัน — ใช้เป็นราวกันตกได้ดี
  • method แบบเขียนจะคืน acknowledgement ที่บอกว่าเกิดอะไรขึ้น (จำนวนนับ, id ที่ insert เข้าไป) อ่านผลลัพธ์ตรงนี้คือวิธียืนยันว่า operation ทำตามที่คุณตั้งใจจริง
  • filter ที่ไม่ตรงกับอะไรเลยไม่ถือเป็น error ถ้า update หรือ delete ไม่ตรงกับ document สักรายการ ผลลัพธ์ก็แค่รายงานว่าเปลี่ยนไป 0 รายการ

โค้ด CRUD จาก AI แทบทุกครั้งจบโดยไม่ดูผลลัพธ์:

🐛 หา bug ในโค้ดนี้

// AI comment: "mark Ada's fines as paid"
async function markPaid(db, name) {
await db.collection("members").updateOne({ nmae: name }, { $set: { fines: 0 } });
return "ok";
}
เฉลย

มี typo ใน filter (nmae) MongoDB ไม่รู้ว่า nmae ไม่ใช่ field มันเลยหา document ที่มี field ชื่อ nmae ไม่พบ ไม่เปลี่ยนอะไร — แล้วรายงานว่าสำเร็จ: { acknowledged: true, matchedCount: 0, modifiedCount: 0 } ฟังก์ชันคืน "ok" และ Ada ยังค้างค่าปรับ วิธีแก้คืออ่าน acknowledgement:

app/crud-check-result.mjs
const members = db.collection("members");
await members.insertOne({ name: "Ada", fines: 3 });
async function markPaid(name, field) {
const res = await members.updateOne({ [field]: name }, { $set: { fines: 0 } });
if (res.matchedCount === 0) throw new Error(`no member matched { ${field}: "${name}" }`);
return res.modifiedCount === 1 ? "paid" : "already zero";
}
let typo = null;
try { await markPaid("Ada", "nmae"); } catch (e) { typo = e.message; }
console.log({ typo, ok: await markPaid("Ada", "name"), again: await markPaid("Ada", "name") });
if (!typo) throw new Error("the typo should have been caught");
{ typo: 'no member matched { nmae: "Ada" }', ok: 'paid', again: 'already zero' }

collection คือกองของ document; ทุกการเรียก CRUD คือ filter + action filter เลือก (ไม่เลือกอะไรไม่ใช่ error), action insert อ่าน เปลี่ยน หรือลบ และ acknowledgement บอกว่าแต่ละขั้นแตะ document กี่รายการ เมื่อจำนวนไม่ตรงที่คาด filter เกือบจะเป็นตัวการเสมอ

ตัวเลือกBenefitCost
method เอกพจน์ (insertOne/updateOne/deleteOne)ควบคุมแม่นยำ กระทบ document เดียว ทำนายง่ายต้องเรียกซ้ำหลายครั้งเพื่อจัดการหลาย document ทำให้เกิด round trip บ่อย
method พหูพจน์ (insertMany/updateMany/deleteMany)จัดการหลาย document ในการเรียกเดียว เร็วกว่ามากfilter หรือ array ที่ผิดพลาดกระทบขอบเขตกว้างกว่าที่ตั้งใจมาก; ไม่ atomic ข้าม document
Hard deleteคืนพื้นที่ทันที เรียบง่ายที่สุดถาวร ไม่มี audit trail
Soft delete (อัปเดต flag)กู้คืนได้ มี audit trailต้องกรอง flag ออกจากทุก query ที่ตามมา
  • “การเขียนที่ไม่รายงาน error แปลว่าสำเร็จ” — filter ที่ typo ด้านบนคืน matchedCount: 0 โดยไม่มี error ตรวจจำนวน
  • “insertMany / updateMany เป็น transaction” — การเขียนแต่ละ document เป็น atomic แต่ batch ไม่ใช่ (คู่มือ CRUD: “All write operations in MongoDB are atomic on the level of a single document”)
  • “วน insert ทีละ document ก็ไม่เป็นไร” — รวมด้วย insertMany (หรือ bulkWrite); แต่ละบทวัดให้ดู
  • “updateOne(filter, { plain: 'doc' }) แทนที่ document” — driver สมัยใหม่โยน error; replaceOne ต่างหากที่แทนที่ (บท update)
  • “shell กับ driver คืน acknowledgement เหมือนกันทุกประการ” — field ต่างกัน (insertedId เทียบ upsertedId, insertedCount บน insertMany)

ตัวอย่างจากของจริง

หน้า CRUD ของคู่มือ MongoDB จัดกลุ่มเดียวกัน — insertOne/insertMany, find, updateOne/updateMany/replaceOne, deleteOne/deleteMany, bulk write — และระบุว่า “All write operations in MongoDB are atomic on the level of a single document” (CRUD operations, เรียกอ่านเมื่อ 2026-09-30)

แท็บ Python/Go/Rust ในโมดูลนี้สะท้อนพฤติกรรมที่พิสูจน์แล้วของ shell/Node และแต่ละแท็บถูกรันหนึ่งครั้งในโปรแกรม scratch (ดูแต่ละบท); ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส

method ใดอ่าน document แรกที่ตรงกับ filter กลับมา?
รูปแบบเอกพจน์ของ method เขียน (เช่น updateOne) รับประกันอะไร?
CRUD ย่อมาจากสี่ operation ใด?
updateOne({ nmae: "Ada" }, { $set: { fines: 0 } }) มี typo ในชื่อ field ผลลัพธ์คืออะไร?
deleteOne ใน mongosh คืน field ใดใน acknowledgement?