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 }"] กลุ่มการทำงานทั้งสี่
หัวข้อที่มีชื่อว่า “กลุ่มการทำงานทั้งสี่”แต่ละกลุ่มมี 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:
db.tabs.insertOne({ name: "Ada", fines: 0 })assert.strictEqual(db.tabs.countDocuments(), 1)await db.collection("members").insertOne({ name: "Ada", fines: 0 });if ((await db.collection("members").countDocuments()) !== 1) throw new Error("expected 1");db.members.insert_one({"name": "Ada", "fines": 0})coll.InsertOne(ctx, bson.M{"name": "Ada", "fines": 0})coll.insert_one(doc! { "name": "Ada", "fines": 0 }).await?;ทุกการเรียก CRUD ตอบด้วย acknowledgement และการอ่านมันคือวิธียืนยันว่าเกิดอะไรขึ้น fence นี้รันหนึ่งการเรียกจากแต่ละกลุ่มใน mongosh และพิมพ์ field ที่ acknowledgement แต่ละอันมี (mongo 8.3.11, mongosh 2.11.1, Mac เครื่องนี้):
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, insertedIdinsertMany: acknowledged, insertedIdsupdateOne : acknowledged, insertedId, matchedCount, modifiedCount, upsertedCountupsert : acknowledged, insertedId, matchedCount, modifiedCount, upsertedCountdeleteOne : acknowledged, deletedCountbulkWrite : acknowledged, insertedCount, insertedIds, matchedCount, modifiedCount, deletedCount, upsertedCount, upsertedIdsfind : 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 ของตัวเอง
โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”บทเรียนถัดไปจะหยิบแต่ละกลุ่มมาทีละกลุ่ม แล้วปิดท้ายด้วยสองท่าไม้ตายที่รวมหลายกลุ่มเข้าด้วยกัน:
- Insert — การเพิ่ม document ทีละรายการหรือทีละหลายรายการ และ
_idอัตโนมัติ - Read —
findเทียบกับfindOne, filter แบบ equality และแบบหลาย field พร้อมแตะ projection เป็นครั้งแรก - Update —
$set,$inc,$unset,$push,$pullและการอ่านจำนวนที่ตรงกัน (match count) ที่การเขียนคืนกลับมา - Delete — การลบ document อย่างแม่นยำ บวกกับรูปแบบ soft-delete
- 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 รายการ
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”โค้ด 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:
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' }Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”collection คือกองของ document; ทุกการเรียก CRUD คือ filter + action filter เลือก (ไม่เลือกอะไรไม่ใช่ error), action insert อ่าน เปลี่ยน หรือลบ และ acknowledgement บอกว่าแต่ละขั้นแตะ document กี่รายการ เมื่อจำนวนไม่ตรงที่คาด filter เกือบจะเป็นตัวการเสมอ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| 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 ที่ตามมา |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “การเขียนที่ไม่รายงาน 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 (ดูแต่ละบท); ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส