Schema validation
MongoDB ยืดหยุ่นเรื่อง schema เป็นค่าเริ่มต้น: collection รับ document รูปทรงต่างกันมาก ซึ่งอิสระตอนทำต้นแบบและอันตรายตอน production Schema validation ให้คุณเลือกกลับมาบังคับ - field ใดต้องมี ชนิดอะไร ค่าใดอนุญาต - โดยไม่ทิ้งโมเดล document ในที่ที่ยังต้องการ คุณแนบกฎด้วย validator $jsonSchema และ server ตรวจทุกการเขียนตามกฎนั้น
ทุกอย่างด้านล่างรันบน mongo 8.3.11, mongosh 2.11.1 และ Node driver 7.7.0 บน Mac เครื่องเดียว; แท็บ Python, Go และ Rust ถูกรันอย่างละครั้งในโปรแกรม scratch บทนี้สร้าง collection members ของตัวเองด้วย validator ที่แสดงตรงนี้
validator หน้าตาเป็นอย่างไร
หัวข้อที่มีชื่อว่า “validator หน้าตาเป็นอย่างไร”validator คือ query expression ที่ทุก document ต้องผ่าน $jsonSchema สร้างจากสาม keyword หลัก: bsonType (ชนิดที่ field หรือ document ต้องเป็น: "object", "string", "int", "array"), required (ชื่อ field ที่ต้องมี) และ properties (แผนที่ต่อ field ของชนิดและข้อจำกัด) ด้านล่าง: สมาชิกทุกคนต้องมี name แบบ string และปี joined แบบ integer ไม่น้อยกว่า 1900 และ fines ถ้ามีต้องเป็นตัวเลขไม่ติดลบ
flowchart LR
W["Incoming write"] --> V{"Matches $jsonSchema?"}
V -->|"Yes"| A["Accepted and stored"]
V -->|"No, action error"| R["Rejected with a validation error"]
V -->|"No, action warn"| L["Stored but logged as a warning"] สร้าง collection พร้อม validator
หัวข้อที่มีชื่อว่า “สร้าง collection พร้อม validator”การแนบ validator เป็นส่วนของการสร้าง collection (collMod เพิ่มทีหลังได้) แท็บ mongosh พิมพ์ options ที่เก็บกลับมา ส่วนแท็บ Node.js คือการสาธิตกับดักเรื่องชนิดข้อมูลที่ครอบคลุมด้านล่าง
db.createCollection("members", { validator: { $jsonSchema: { bsonType: "object", required: ["name", "joined"], properties: { name: { bsonType: "string" }, joined: { bsonType: "int", minimum: 1900 }, fines: { bsonType: "number", minimum: 0 } } } }, validationLevel: "strict", validationAction: "error"});console.log(db.getCollectionInfos({ name: "members" })[0].options);db.members.insertOne({ name: "Ada", joined: 2024, fines: 0 });console.log("valid document stored:", db.members.countDocuments());const members = db.collection("members");await db.createCollection("members", { validator: { $jsonSchema: { bsonType: "object", required: ["name", "joined"], properties: { name: { bsonType: "string" }, joined: { bsonType: "int", minimum: 1900 } } } } });const tryInsert = async (label, doc) => { try { await members.insertOne(doc); console.log(label.padEnd(24), "stored"); return true; } catch (e) { console.log(label.padEnd(24), "REJECTED", e.code, e.errInfo.details.schemaRulesNotSatisfied[0].propertiesNotSatisfied[0].details[0].consideredType); return false; } };const a = await tryInsert("joined: 2024", { name: "a", joined: 2024 });const b = await tryInsert("joined: new Double(2024)", { name: "b", joined: new Double(2024) });const c = await tryInsert("joined: 2 ** 31", { name: "c", joined: 2 ** 31 });const d = await tryInsert("joined: new Int32(2024)", { name: "d", joined: new Int32(2024) });if (!(a && !b && !c && d)) throw new Error("unexpected type behaviour");db.create_collection( "members", validator={"$jsonSchema": { "bsonType": "object", "required": ["name", "joined"], "properties": { "name": {"bsonType": "string"}, "joined": {"bsonType": "int", "minimum": 1900}, "fines": {"bsonType": "number", "minimum": 0}, }, }}, validationLevel="strict", validationAction="error",)validator := bson.M{"$jsonSchema": bson.M{ "bsonType": "object", "required": bson.A{"name", "joined"}, "properties": bson.M{ "name": bson.M{"bsonType": "string"}, "joined": bson.M{"bsonType": "int", "minimum": 1900}, "fines": bson.M{"bsonType": "number", "minimum": 0}, },}}opts := options.CreateCollection(). SetValidator(validator). SetValidationLevel("strict"). SetValidationAction("error")if err := db.CreateCollection(ctx, "members", opts); err != nil { return err}db.create_collection("members") .validator(doc! { "$jsonSchema": { "bsonType": "object", "required": ["name", "joined"], "properties": { "name": { "bsonType": "string" }, "joined": { "bsonType": "int", "minimum": 1900 }, "fines": { "bsonType": "number", "minimum": 0 }, }, } }) .validation_level(ValidationLevel::Strict) .validation_action(ValidationAction::Error) .await?;{ validator: { '$jsonSchema': { bsonType: 'object', required: [ 'name', 'joined' ], properties: { name: { bsonType: 'string' }, joined: { bsonType: 'int', minimum: 1900 }, fines: { bsonType: 'number', minimum: 0 } } } }, validationLevel: 'strict', validationAction: 'error'}valid document stored: 1การเขียนที่ถูกปฏิเสธ และอ่านสาเหตุอย่างไร
หัวข้อที่มีชื่อว่า “การเขียนที่ถูกปฏิเสธ และอ่านสาเหตุอย่างไร”document ที่ผิดกฎถูกปฏิเสธด้วย error code 121 และข้อความเปล่า ๆ Document failed validation เหตุผลอยู่ใน error.errInfo.details ซึ่งแสดงทุกกฎที่ล้มเหลวพร้อมค่าและชนิดที่ server พิจารณา:
try { db.members.insertOne({ name: 42 }); }catch (e) { console.log(e.code, e.codeName || "", "-", e.message); console.log(JSON.stringify(e.errInfo.details.schemaRulesNotSatisfied, null, 1)); }try { db.members.insertOne({ name: "Zed", joined: 1800, fines: -1 }); }catch (e) { console.log("second try:", JSON.stringify(e.errInfo.details.schemaRulesNotSatisfied[0].propertiesNotSatisfied.map((p) => p.propertyName + ": " + p.details[0].operatorName + " " + p.details[0].reason))); }try { db.members.insertOne({ name: "Zed", joined: 2024.5 }); }catch (e) { console.log("third try :", e.errInfo.details.schemaRulesNotSatisfied[0].propertiesNotSatisfied[0].details[0].reason, "- considered type:", e.errInfo.details.schemaRulesNotSatisfied[0].propertiesNotSatisfied[0].details[0].consideredType); }assert.strictEqual(db.members.countDocuments(), 1);121 - Document failed validation[ { "operatorName": "properties", "propertiesNotSatisfied": [ { "propertyName": "name", "details": [ { "operatorName": "bsonType", "specifiedAs": { "bsonType": "string" }, "reason": "type did not match", "consideredValue": 42, "consideredType": "int" } ] } ] }, { "operatorName": "required", "specifiedAs": { "required": [ "name", "joined" ] }, "missingProperties": [ "joined" ] }]second try: ["joined: minimum comparison failed","fines: minimum comparison failed"]third try : type did not match - considered type: doubleกับดักชนิดตัวเลข
หัวข้อที่มีชื่อว่า “กับดักชนิดตัวเลข”bsonType เข้มงวดกว่า JSON: "int", "long", "double" และ "decimal" ต่างกัน และ "number" รับทั้งหมด server เห็นชนิดไหนขึ้นกับ driver mongosh เก็บ 2024 เป็น int; driver Node.js เก็บตัวเลข JavaScript ในช่วง 32 บิตเป็น int32 และที่เหลือ (ทศนิยม หรือ 2 ** 31) เป็น double:
joined: 2024 storedjoined: new Double(2024) REJECTED 121 doublejoined: 2 ** 31 REJECTED 121 doublejoined: new Int32(2024) storedvalidationLevel และ validationAction
หัวข้อที่มีชื่อว่า “validationLevel และ validationAction”สองปุ่มควบคุมว่ากฎกัดแค่ไหน validationLevel ตัดสินว่าตรวจการเขียน ไหน: "strict" (ค่าเริ่มต้น) ตรวจทุก insert และ update; "moderate" ตรวจ insert และ update ของ document ที่ผ่านกฎอยู่แล้ว และไม่แตะ document ที่ไม่ถูกต้องอยู่เดิม validationAction ตัดสินว่าเมื่อล้มเหลวเกิดอะไร: "error" (ค่าเริ่มต้น) ปฏิเสธ; "warn" อนุญาตและบันทึก log document สามรายการที่เขียน ก่อน มีกฎ (ถูกหนึ่ง ผิดสอง) ภายใต้แต่ละ level:
db.legacy.insertMany([{ _id: 1, name: "ok", joined: 2020 }, { _id: 2, name: 7 }, { _id: 3, joined: 1500 }]); // written before any rules existedconst rules = { $jsonSchema: { bsonType: "object", required: ["name", "joined"], properties: { name: { bsonType: "string" }, joined: { bsonType: "int", minimum: 1900 } } } };const attempt = (label, f) => { try { const r = f(); console.log(label.padEnd(46), "allowed, modified:", r.modifiedCount ?? r.insertedId ?? "yes"); } catch (e) { console.log(label.padEnd(46), "REJECTED", e.code); } };for (const level of ["strict", "moderate"]) { db.runCommand({ collMod: "legacy", validator: rules, validationLevel: level, validationAction: "error" }); console.log("--- validationLevel:", level); attempt("update the VALID doc so it breaks the rules", () => db.legacy.updateOne({ _id: 1 }, { $set: { joined: 1500 } })); db.legacy.updateOne({ _id: 1 }, { $set: { name: "ok", joined: 2020 } }, { bypassDocumentValidation: true }); attempt("update an already INVALID doc (still invalid)", () => db.legacy.updateOne({ _id: 2 }, { $set: { note: "x" } })); attempt("insert a new invalid doc", () => db.legacy.insertOne({ name: 1 })); db.legacy.deleteMany({ name: 1 });}--- validationLevel: strictupdate the VALID doc so it breaks the rules REJECTED 121update an already INVALID doc (still invalid) REJECTED 121insert a new invalid doc REJECTED 121--- validationLevel: moderateupdate the VALID doc so it breaks the rules REJECTED 121update an already INVALID doc (still invalid) allowed, modified: 1insert a new invalid doc REJECTED 121การ rollout ที่พบบ่อยคือ "moderate" กับ "warn" ก่อน ดู log เพื่อรู้ว่าข้อมูลเดิมผิดกฎเท่าไร แก้ตัวที่ผิด แล้วค่อยเข้มเป็น "strict" และ "error" บรรทัด log warn จริง:
db.runCommand({ collMod: "members", validationAction: "warn" });db.members.insertOne({ name: "warned", joined: "not a number" }); // stored anywayconsole.log("stored despite the rules:", db.members.countDocuments({ name: "warned" }));const line = db.adminCommand({ getLog: "global" }).log.filter((l) => l.includes("Document would fail validation")).pop();const j = JSON.parse(line);console.log({ severity: j.s, component: j.c, id: j.id, msg: j.msg, namespace: j.attr.namespace.split(".").pop(), firstBrokenRule: j.attr.errInfo.details.schemaRulesNotSatisfied[0].operatorName });db.runCommand({ collMod: "members", validationAction: "error" });assert.strictEqual(j.id, 20294);stored despite the rules: 1{ severity: 'W', component: 'STORAGE', id: 20294, msg: 'Document would fail validation', namespace: 'members', firstBrokenRule: 'properties'}บรรทัดเดียวกันถูกเขียนลง log ของ mongod (docker logs <container> แสดง) และ getLog อ่านผ่านเชลล์ได้
เปลี่ยนกฎภายหลัง: collMod และ bypass
หัวข้อที่มีชื่อว่า “เปลี่ยนกฎภายหลัง: collMod และ bypass”const opts = () => db.getCollectionInfos({ name: "members" })[0].options;console.log("now :", opts().validationLevel, opts().validationAction);db.runCommand({ collMod: "members", validationLevel: "moderate", validationAction: "warn" });console.log("relaxed :", opts().validationLevel, opts().validationAction);db.runCommand({ collMod: "members", validator: {} });console.log("removed :", "validator =", JSON.stringify(opts().validator ?? null));db.members.insertOne({ anything: "goes" });console.log("free-form insert stored:", db.members.countDocuments({ anything: "goes" }));db.members.deleteOne({ anything: "goes" });now : strict errorrelaxed : moderate warnremoved : validator = nullfree-form insert stored: 1validation ไม่ใช่ประตูทางเดียว สำหรับการโหลดจำนวนมากหรือ migration ที่เชื่อถือได้ option bypassDocumentValidation ข้ามกฎสำหรับการเขียนนั้น (เป็นสิทธิ์เฉพาะเมื่อเปิด authentication):
db.runCommand({ collMod: "members", validator: { $jsonSchema: { bsonType: "object", required: ["name", "joined"], properties: { name: { bsonType: "string" }, joined: { bsonType: "int", minimum: 1900 } } } }, validationLevel: "strict", validationAction: "error" });const doc = { name: "migration", joined: 1000 };try { db.members.insertOne(doc); } catch (e) { console.log("normal insert :", "REJECTED", e.code); }const r = db.runCommand({ insert: "members", documents: [doc], bypassDocumentValidation: true });console.log("with bypassDocumentValidation: n =", r.n, "(needs the bypassDocumentValidation privilege when auth is on)");assert.strictEqual(r.n, 1);normal insert : REJECTED 121with bypassDocumentValidation: n = 1 (needs the bypassDocumentValidation privilege when auth is on)In Compass: เปิด collection แล้วไปที่แท็บ Validation วางหรือแก้ document $jsonSchema เลือก level และ action จากดรอปดาวน์ และ Compass แสดงตัวอย่างว่า document เดิมใดผ่านหรือไม่ผ่านก่อนบันทึก
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- validation เป็นแบบเลือกเปิดและต่อ collection; ไม่มี validator ก็ไม่มีอะไรถูกบังคับ
bsonTypeเข้มงวด: double ที่คุณกำหนดให้เป็น int ล้มเหลว (2 ** 31จาก Node,2024.5) ใช้"number"เมื่อชนิดตัวเลขใดก็ได้requiredตรวจว่ามี;propertiesตรวจชนิดและค่า ใช้ทั้งคู่- เพิ่ม validation ให้ collection ที่มีอยู่ด้วย
"moderate"+"warn"เพื่อไม่ให้ document เก่าทำให้การเขียนพัง - มีเพียง
bsonTypeที่รองรับชนิด BSON;type: "integer"แบบ JSON Schema ไม่รองรับ
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”เมื่อถูกขอ validator สำหรับปีที่เข้าร่วมแบบจำนวนเต็ม AI ใช้คำศัพท์ของ JSON Schema:
🐛 หา bug ในโค้ดนี้
// AI comment: "integer is the JSON Schema type for whole numbers"await db.createCollection("strictMembers", {validator: { $jsonSchema: { bsonType: "object", properties: { joined: { type: "integer", minimum: 1900 } } } },});เฉลย
MongoDB implement ชุดย่อยของ JSON Schema พร้อมส่วนขยาย BSON: keyword integer ของ type ถูกปฏิเสธตอนสร้าง collection (FailedToParse: $jsonSchema type 'integer' is not currently supported) ชื่อ BSON คือ bsonType: "int" (หรือ "long" หรือ "number" สำหรับตัวเลขชนิดใดก็ได้) โปรดสังเกตกับดักลำดับที่สองจากแท็บ Node.js: กับ "int" ค่าที่ driver ของคุณเข้ารหัสเป็น double จะถูกปฏิเสธตอนรัน ดังนั้นให้ตรวจสิ่งที่ driver ส่งจริง:
// AI: JSON-Schema habit - "integer" is the type for whole numberstry { db.createCollection("strictMembers", { validator: { $jsonSchema: { bsonType: "object", properties: { joined: { type: "integer", minimum: 1900 } } } } }); }catch (e) { console.log(e.codeName + ":", e.message); }db.createCollection("strictMembers", { validator: { $jsonSchema: { bsonType: "object", properties: { joined: { bsonType: "int", minimum: 1900 } } } } });console.log("with bsonType: 'int' it works:", db.getCollectionInfos({ name: "strictMembers" }).length === 1);FailedToParse: $jsonSchema type 'integer' is not currently supported.with bsonType: 'int' it works: trueMental Model
หัวข้อที่มีชื่อว่า “Mental Model”validator คือตัวกรองบนเส้นทางเขียน: ทุก insert และ update ถูกทดสอบกับกฎรูปแบบ query ก่อนลงจริง validationLevel เลือกว่าการเขียนใดต้องผ่านการทดสอบ validationAction เลือกผลที่ตามมา และ collMod หมุนปุ่ม กฎมองเห็นชนิด BSON ไม่ใช่ชนิด JavaScript - driver ที่คุณใช้จึงเป็นส่วนหนึ่งของ schema
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
$jsonSchema + error | การเขียนที่ผิดรูปไม่ลงเลย | กฎต้องตามโค้ดแอป; ชนิดไม่ตรงจาก driver โผล่เป็น 121 |
moderate + warn | rollout ปลอดภัยบนข้อมูลเก่า; การละเมิดอยู่ใน log | ข้อมูลผิดยังเข้าได้; ต้องมีคนอ่าน log |
strict | document เก่าที่ผิดแก้ไขไม่ได้ถ้าไม่ซ่อม | ทำให้ update document เก่าพัง |
bypassDocumentValidation | migration และการโหลดจำนวนมาก | ข้ามตาข่ายนิรภัย; ต้องมีสิทธิ์เมื่อเปิด auth |
| ไม่มี validator | ยืดหยุ่นเต็มที่ | ข้อมูลผิดรูปถึง production โดยไม่มีใครสังเกต |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “validator แทนการตรวจสอบในแอปได้” - มันตรวจรูปทรงและข้อจำกัดง่าย ๆ ไม่ใช่กฎอย่างความไม่ซ้ำ (ใช้ unique index)
- “
type: 'integer'ใช้ได้เหมือน JSON Schema” - parse ไม่ผ่าน; ใช้bsonType - “จำนวนเต็มของ JavaScript เป็น int เสมอ” -
2 ** 31และทศนิยมทุกตัวถึง server เป็น double และไม่ผ่านbsonType: "int" - “
moderateข้าม validation” - insert document ที่ผิดและ update ที่ทำให้ document ที่ถูกกลายเป็นผิดยังถูกปฏิเสธ - “validator ซิงก์กับโค้ดของฉัน” - ไม่ซิงก์; เปลี่ยนแอปโดยไม่
collModกฎเก่ายังอยู่
ตัวอย่างจากของจริง
คู่มือ schema validation ของ MongoDB นิยามพฤติกรรมที่วัดข้างบน: ใน “moderate” “updates to existing documents in the collection that don’t match the validation rules aren’t required to pass validation” ส่วน “strict” “applies the same validation rules to all document inserts and updates” (validation level, ตรวจ 2026-09-30)
แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่พิสูจน์แล้วของ shell/Node; แต่ละแท็บถูกรันหนึ่งครั้งในโปรแกรม scratch กับ server เดียวกัน (สร้าง collection พร้อม validator และ document ที่ผิดถูกปฏิเสธ) แต่ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส