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

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 คือ 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"]
ทุกการเขียนถูกตรวจกับ validator; validationAction ตัดสินว่าความล้มเหลวถูกปฏิเสธหรือแค่บันทึก log

การแนบ validator เป็นส่วนของการสร้าง collection (collMod เพิ่มทีหลังได้) แท็บ mongosh พิมพ์ options ที่เก็บกลับมา ส่วนแท็บ Node.js คือการสาธิตกับดักเรื่องชนิดข้อมูลที่ครอบคลุมด้านล่าง

mongosh/val-create.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());
{
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 พิจารณา:

mongosh/val-reject.js
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 stored
joined: new Double(2024) REJECTED 121 double
joined: 2 ** 31 REJECTED 121 double
joined: new Int32(2024) stored

สองปุ่มควบคุมว่ากฎกัดแค่ไหน validationLevel ตัดสินว่าตรวจการเขียน ไหน: "strict" (ค่าเริ่มต้น) ตรวจทุก insert และ update; "moderate" ตรวจ insert และ update ของ document ที่ผ่านกฎอยู่แล้ว และไม่แตะ document ที่ไม่ถูกต้องอยู่เดิม validationAction ตัดสินว่าเมื่อล้มเหลวเกิดอะไร: "error" (ค่าเริ่มต้น) ปฏิเสธ; "warn" อนุญาตและบันทึก log document สามรายการที่เขียน ก่อน มีกฎ (ถูกหนึ่ง ผิดสอง) ภายใต้แต่ละ level:

mongosh/val-levels.js
db.legacy.insertMany([{ _id: 1, name: "ok", joined: 2020 }, { _id: 2, name: 7 }, { _id: 3, joined: 1500 }]); // written before any rules existed
const 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: strict
update the VALID doc so it breaks the rules REJECTED 121
update an already INVALID doc (still invalid) REJECTED 121
insert a new invalid doc REJECTED 121
--- validationLevel: moderate
update the VALID doc so it breaks the rules REJECTED 121
update an already INVALID doc (still invalid) allowed, modified: 1
insert a new invalid doc REJECTED 121

การ rollout ที่พบบ่อยคือ "moderate" กับ "warn" ก่อน ดู log เพื่อรู้ว่าข้อมูลเดิมผิดกฎเท่าไร แก้ตัวที่ผิด แล้วค่อยเข้มเป็น "strict" และ "error" บรรทัด log warn จริง:

mongosh/val-warn.js
db.runCommand({ collMod: "members", validationAction: "warn" });
db.members.insertOne({ name: "warned", joined: "not a number" }); // stored anyway
console.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 อ่านผ่านเชลล์ได้

mongosh/val-collmod.js
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 error
relaxed : moderate warn
removed : validator = null
free-form insert stored: 1

validation ไม่ใช่ประตูทางเดียว สำหรับการโหลดจำนวนมากหรือ migration ที่เชื่อถือได้ option bypassDocumentValidation ข้ามกฎสำหรับการเขียนนั้น (เป็นสิทธิ์เฉพาะเมื่อเปิด authentication):

mongosh/val-bypass.js
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 121
with 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 ไม่รองรับ

เมื่อถูกขอ 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 ส่งจริง:

mongosh/val-bug.js
// AI: JSON-Schema habit - "integer" is the type for whole numbers
try { 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: true

validator คือตัวกรองบนเส้นทางเขียน: ทุก insert และ update ถูกทดสอบกับกฎรูปแบบ query ก่อนลงจริง validationLevel เลือกว่าการเขียนใดต้องผ่านการทดสอบ validationAction เลือกผลที่ตามมา และ collMod หมุนปุ่ม กฎมองเห็นชนิด BSON ไม่ใช่ชนิด JavaScript - driver ที่คุณใช้จึงเป็นส่วนหนึ่งของ schema

ทางเลือกข้อดีต้นทุน
$jsonSchema + errorการเขียนที่ผิดรูปไม่ลงเลยกฎต้องตามโค้ดแอป; ชนิดไม่ตรงจาก driver โผล่เป็น 121
moderate + warnrollout ปลอดภัยบนข้อมูลเก่า; การละเมิดอยู่ใน logข้อมูลผิดยังเข้าได้; ต้องมีคนอ่าน log
strictdocument เก่าที่ผิดแก้ไขไม่ได้ถ้าไม่ซ่อมทำให้ update document เก่าพัง
bypassDocumentValidationmigration และการโหลดจำนวนมากข้ามตาข่ายนิรภัย; ต้องมีสิทธิ์เมื่อเปิด auth
ไม่มี validatorยืดหยุ่นเต็มที่ข้อมูลผิดรูปถึง production โดยไม่มีใครสังเกต
  • “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 ที่ผิดถูกปฏิเสธ) แต่ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส

keyword bsonType ใดกำหนดให้เป็นจำนวนเต็มที่เก็บเป็น integer 32 บิต
Node.js insert { joined: 2 ** 31 } ลง collection ที่กำหนด bsonType "int" เกิดอะไรขึ้น
validationLevel "strict" ต่างจาก "moderate" อย่างไรบน collection ที่มี document เก่าที่ไม่ถูกต้อง
เมื่อ validationAction: "warn" document ที่ผิดกฎเกิดอะไรขึ้น
จะอ่านสาเหตุที่การเขียนถูกปฏิเสธได้อย่างไร
คำสั่งใดเปลี่ยนหรือลบ validator ภายหลัง