MongoDB: Schema Validation:数据库层数据校验
Schema Validation 是数据库层的数据校验——即使没有应用层也能拦截脏数据。
数据库层校验的独特价值:为什么有了 mongoose 校验器还需要 Schema Validation?因为 mongoose 校验只在应用层生效——1. 多应用接入:如果多个微服务(Node.js/Python/Go)写入同一个 MongoDB,只有 Node.js 服务有 mongoose 校验,其他服务没有;2. 直接数据库操作:运维用 mongo shell 修复数据、ETL 脚本直接写入,都绕过 mongoose;3. 防御纵深:即使应用层校验有 bug,数据库层仍能拦截。Schema Validation 是最后防线——平时不触发(应用层已拦截),但在异常情况下保护数据完整性。
Schema Validation 的局限性与补偿:MongoDB Schema Validation 有明确局限——1. 不支持跨字段校验(如"endDate > startDate"),需应用层补充;2. 不支持异步校验(如"username 唯一"需查数据库),需唯一索引补充;3. 不支持条件校验(如"type='book' 时 author 必填"),需应用层补充;4. $jsonSchema 不支持所有 MongoDB 操作符(如 $regex 受限)。因此 Schema Validation 不能完全替代应用层校验——正确的做法是应用层做丰富校验(友好的错误提示、跨字段逻辑、异步验证),数据库层做兜底校验(必填/类型/范围/唯一性)。
1. 你将学到
- $jsonSchema 验证器
- validator action(error / warn)
- validationLevel(strict / moderate)
- mongoose Schema vs MongoDB $jsonSchema 对比
- Schema 演进策略
graph LR
A[客户端插入文档] --> B{mongo<br/>Schema Validation}
B -->|validationLevel<br/>strict/moderate| C{验证规则}
C -->|bsonType| D[类型检查]
C -->|required| E[必填检查]
C -->|pattern| F[正则检查]
C -->|enum| G[枚举检查]
C -->|minLength| H[长度检查]
D --> I{通过?}
E --> I
F --> I
G --> I
H --> I
I -->|是 + action=error| J[✅ 插入成功]
I -->|否 + action=error| K[❌ 拒绝 + 抛错]
I -->|否 + action=warn| L[⚠️ 允许 + 警告]
style J fill:#d4edda
style K fill:#f8d7da
2. $jsonSchema 验证器
概念说明:$jsonSchema 是 MongoDB 3.6+ 引入的文档结构校验语言,基于 JSON Schema 规范。它允许在数据库层定义文档必须满足的结构规则——字段类型、必填字段、值范围、正则模式等。与应用层校验不同,$jsonSchema 由 mongod 引擎强制执行,任何客户端(Python/Java/Node.js)写入都必须遵守。
工作原理:创建带 validator 的集合时,MongoDB 将 $jsonSchema 规则存储在集合元数据中。每次 insert/update 操作,引擎在写入前自动校验文档是否满足规则。校验失败时,根据 validationAction 决定是抛错拒绝还是记录警告。
$jsonSchema 核心关键字:
| 关键字 | 作用 | 示例 |
|---|---|---|
bsonType |
指定 BSON 类型 | 'string', 'int', 'object', 'array' |
required |
必填字段列表 | ['email', 'username'] |
properties |
字段级规则定义 | { email: { bsonType: 'string' } } |
pattern |
正则校验 | '^.+@.+$' (邮箱格式) |
enum |
枚举值 | ['customer', 'admin'] |
minimum / maximum |
数值范围 | minimum: 0, maximum: 150 |
minLength / maxLength |
字符串长度 | minLength: 3, maxLength: 30 |
items |
数组元素规则 | { bsonType: 'string' } |
minItems |
数组最小长度 | minItems: 1 |
$jsonSchema 与 JSON Schema 的关系:MongoDB 的 $jsonSchema 基于 JSON Schema Draft 4 规范,但有几个关键差异——1. 用 bsonType 替代 type(因为 JSON Schema 不区分 int/double/decimal/objectId 等 BSON 类型);2. additionalProperties 默认 true(允许未定义的字段,与 JSON Schema Draft 4 默认不同);3. 不支持 $ref 引用(所有规则必须内联定义);4. 不支持 format(如 email/uri/date-time,需用 pattern 正则替代)。理解这些差异避免"照抄 JSON Schema 教程代码却报错"的困惑。
$jsonSchema 的嵌套验证:$jsonSchema 支持嵌套对象和数组的递归验证——1. 嵌套对象用 properties + required 定义子结构(如 address: {bsonType: 'object', required: ['city'], properties: {city: {bsonType: 'string'}}});2. 数组用 items 定义元素规则(如 tags: {bsonType: 'array', items: {bsonType: 'string'}} 验证数组元素都是字符串);3. 嵌套深度无硬性限制,但过深的嵌套影响验证性能和可读性——3 层以上考虑拆分为独立集合。嵌套验证是文档模型的核心优势——SQL 需要多表 JOIN 才能验证关联数据,MongoDB 一次验证整个文档树。
使用场景:
- 关键数据结构保护(用户、订单、支付记录)
- 多语言微服务共享数据库时的统一校验
- 防止脏数据写入(兜底应用层校验遗漏)
- 不适合:需要异步校验、跨集合校验、复杂业务逻辑校验
$jsonSchema 规范与 JSON Schema 的关系:MongoDB 的 $jsonSchema 基于 JSON Schema Draft 4 规范,但做了 BSON 扩展——用 bsonType 替代 type(因为 MongoDB 的类型是 BSON 而非 JSON),增加了 objectId、decimal、date 等 BSON 专用类型。理解这个关系很重要:1. bsonType: 'string' 对应 JSON Schema 的 type: 'string';2. bsonType: 'int' 没有对应(JSON 只有 number);3. required/properties/pattern/enum 与 JSON Schema 完全一致。
版本迁移策略:Schema Validation 的修改需要版本化策略——1. 新增可选字段:低风险,直接 moderate + warn 上线;2. 新增必填字段:中风险,先 optional → 数据迁移 → 再 required;3. 修改字段类型:高风险,双写字段 → 迁移 → 切换 → 删旧字段;4. 收紧值范围:中风险,先 moderate + warn 观察 → 确认无大量违规 → 切换 error。每次变更记录旧规则,必要时可 collMod 回退。
// === 创建带验证的集合 ===
db.createCollection('users', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'],
properties: {
email: {
bsonType: 'string',
pattern: '^.+@.+$',
maxLength: 100
},
username: {
bsonType: 'string',
minLength: 3,
maxLength: 30
},
age: {
bsonType: 'int',
minimum: 0,
maximum: 150
},
role: {
enum: ['customer', 'admin', 'moderator']
},
isActive: {
bsonType: 'bool'
}
}
}
},
validationLevel: 'strict',
validationAction: 'error'
});
要点解析:
bsonType与 JSON Schema 的type不同,MongoDB 使用 BSON 类型名(如'int'而非'number')required是顶层关键字,值为字段名数组,不属于任何 property- 嵌套文档通过
properties嵌套定义,数组元素通过items定义
嵌套验证的设计模式:$jsonSchema 的嵌套验证有三种设计模式——1. 全内联模式(地址和订单项直接嵌套在订单的 $jsonSchema 中,结构清晰但文件冗长);2. 变量抽取模式(将 addressSchema 和 itemSchema 定义为 JavaScript 变量,在主 Schema 中引用,代码复用性好但需要在应用层管理);3. 混合模式(核心字段内联、可复用子结构抽取为变量)。生产环境推荐模式 3——地址和订单项可能被多个集合复用(订单和用户都有地址),抽取为独立变量减少重复定义。
数组验证的边界情况:$jsonSchema 的数组验证有几个容易出错的边界情况——1. minItems/maxItems 检查的是数组长度而非文档数量(空数组 [] 通过 minItems: 0 但不通过 minItems: 1);2. items 定义的是所有元素的规则(不支持"前 3 个元素不同类型"的元组验证,JSON Schema Draft 4 支持 but MongoDB 不支持);3. uniqueItems: true 检查数组元素唯一性但对嵌套对象可能不按预期工作(对象按引用比较而非深度比较);4. 空数组 vs null 数组——空数组 [] 通过 bsonType: 'array' 验证,但 null 不通过(需 bsonType: ['array', 'null'] 允许 null)。
▶ 示例 1: 嵌套文档 + 数组校验
// ShopHub 订单集合:嵌套地址 + 订单项数组校验
db.createCollection('orders', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['userId', 'items', 'total', 'address'],
properties: {
userId: { bsonType: 'objectId' },
items: {
bsonType: 'array',
minItems: 1,
items: {
bsonType: 'object',
required: ['productId', 'qty', 'price'],
properties: {
productId: { bsonType: 'objectId' },
qty: { bsonType: 'int', minimum: 1 },
price: { bsonType: 'decimal', minimum: 0 }
}
}
},
address: {
bsonType: 'object',
required: ['street', 'city', 'zipCode'],
properties: {
street: { bsonType: 'string', minLength: 1 },
city: { bsonType: 'string' },
zipCode: { bsonType: 'string', pattern: '^[0-9]{5,10}$' }
}
},
total: { bsonType: 'decimal', minimum: 0 }
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
// 测试:合法订单
db.orders.insertOne({
userId: ObjectId(),
items: [{ productId: ObjectId(), qty: Int32(2), price: Decimal128('29.99') }],
address: { street: '123 Main St', city: 'Seattle', zipCode: '98101' },
total: Decimal128('59.98')
});
// ✅ 成功
// 测试:空 items 数组
db.orders.insertOne({
userId: ObjectId(),
items: [],
address: { street: '123 Main St', city: 'Seattle', zipCode: '98101' },
total: Decimal128('0')
});
// ❌ Document failed validation (minItems: 1)
输出:
// 执行成功
3. validationAction
概念说明:validationAction 控制校验失败时 MongoDB 的行为——是严格拒绝(error)还是宽松放行并记录警告(warn)。这是数据完整性与业务连续性之间的关键权衡点。
工作原理:
error模式:校验失败时抛出DocumentFailedValidation错误,写入操作被回滚,客户端收到异常warn模式:校验失败时写入仍然成功,但在 mongod 日志中记录一条警告信息,适合渐进式上线
warn 模式的运维价值:warn 模式的核心价值是"零风险上线新规则"——添加新验证规则时先设 warn,观察 1-2 周日志,统计有多少现有写入会被拒绝。如果违规率<1%,说明规则安全,可以切为 error;如果违规率>5%,说明需要调整规则或先清理历史数据。这种渐进式上线策略避免了"一上线就大面积报错"的生产事故。warn 日志查询:db.adminCommand({getLog: 'global'}) 过滤 DocumentFailedValidation 关键词。
warn 日志的监控方案:warn 模式的日志需要主动监控——1. 日志过滤:MongoDB 的 warn 日志混在其他日志中,用 Filebeat/Fluentd 采集后过滤 DocumentFailedValidation 关键词;2. 告警规则:每小时违规数 > 10 触发 Slack/邮件告警(说明规则可能过于严格);3. 违规统计面板:按集合、字段、错误类型分组统计违规次数,判断哪些规则需要调整;4. 自动化报告:每天生成违规摘要报告(X 集合 Y 字段违规 Z 次),发送给 DBA 和后端团队。warn 模式不是"设了就不管",而是"设了就密切观察"——只有持续监控才能安全地将 warn 切为 error。
使用场景:
| 阶段 | 推荐 action | 原因 |
|---|---|---|
| 开发/测试 | error | 尽早发现数据问题 |
| 新规则上线初期 | warn | 避免阻断业务,观察违规情况 |
| 规则稳定后 | error | 强制执行,确保数据完整性 |
| 数据迁移 | off | 临时关闭,避免旧数据被拒绝 |
graph LR
A[写入操作] --> B{Schema Validation}
B -->|通过| C[✅ 写入成功]
B -->|失败 + action=error| D[❌ 抛错拒绝]
B -->|失败 + action=warn| E[⚠️ 写入成功 + 日志警告]
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#fff3cd
| action | 行为 | 适用场景 |
|---|---|---|
error |
拒绝插入/更新(抛错) | 生产环境,数据完整性优先 |
warn |
允许但记录警告(不抛错) | 渐进上线,观察期 |
// === error 模式(推荐生产)===
db.createCollection('users', {
validator: { $jsonSchema: {...} },
validationAction: 'error'
});
// === warn 模式(宽松)===
db.createCollection('users', {
validator: { $jsonSchema: {...} },
validationAction: 'warn'
});
// 插入不符合的文档:成功 + 警告日志
要点解析:
- 从 warn 切换到 error 前,建议先分析 warn 日志中的违规频率
- warn 模式的日志可通过
db.adminCommand({getLog:'global'})查看 - 不存在
validationAction: 'off',关闭校验需设validationLevel: 'off'
4. validationLevel
概念说明:validationLevel 决定校验规则应用于哪些文档——仅新文档(moderate)还是包括已存在的旧文档(strict)。这是 Schema 演进的核心配置,决定了新增规则对存量数据的影响。
工作原理:
strict:所有 insert 和 update 都校验,包括修改已有文档时moderate:仅对新插入的文档和已满足验证规则的文档的 update 进行校验;不满足规则的历史文档更新时不校验off:完全关闭校验
使用场景:
| 场景 | 推荐 level | 原因 |
|---|---|---|
| 全新集合 | strict | 无历史包袱,全面校验 |
| 已有集合新增规则 | moderate | 避免旧数据无法更新 |
| 数据迁移中 | off | 临时关闭,迁完再开 |
| 规则稳定 + 数据干净 | strict | 最强保护 |
| level | 行为 | 适用场景 |
|---|---|---|
strict |
验证所有文档(包括已存在的) | 新集合、数据干净 |
moderate |
仅验证新插入/更新的文档(推荐) | 已有集合、渐进上线 |
off |
不验证 | 数据迁移 |
// === moderate 模式(推荐)===
db.createCollection('users', {
validator: { $jsonSchema: {...} },
validationLevel: 'moderate'
});
// 已存在的脏数据不验证,仅验证新数据
要点解析:
- moderate 是生产环境最常用的 level,它不会阻断对历史脏数据的更新操作
- 从 moderate 切换到 strict 前,需要先清理不合规的历史数据
collMod可以动态修改 validationLevel,无需重建集合
moderate 的隐含风险:moderate 模式的"仅对新数据验证"看似安全,但有隐含风险——1. 旧数据可以无限次更新而不触发验证,脏数据可能越来越脏;2. 应用代码可能依赖 Schema 规则(如假设所有文档都有 email 字段),旧数据不符合时应用崩溃;3. moderate 不是"安全默认值",而是"迁移缓冲期"——应在迁移完成后切为 strict。最佳实践:先用 moderate + warn 观察违规率,违规率降为 0 后切为 strict + error。
数据迁移与验证的配合:Schema 验证与数据迁移需要配合——1. 新增 required 字段时先设 default 值(避免旧文档缺少必填字段),再用迁移脚本回填历史数据;2. 新增 enum 约束时先确定所有历史值都在枚举范围内(否则 moderate 模式下旧数据可更新但 strict 模式下会报错);3. 收紧约束(如 maxlength 从 200 改为 100)时先用 warn 模式观察,确认无超长数据后再切 error。迁移脚本通常用 bulkWrite + $set 批量更新历史数据。
5. 修改已存在集合的验证器
概念说明:生产环境中 Schema 不是一成不变的——业务迭代需要新增字段、修改规则、甚至删除约束。collMod 命令允许在线修改已有集合的验证器,无需重建集合、无需停服。
collMod 的注意事项:collMod 修改验证器是"全量替换"而非"增量合并"——每次调用必须提供完整的 $jsonSchema 规则,即使只改一个字段。这意味着:1. 修改前必须保存当前规则(db.getCollectionInfos() 查看现有验证器);2. 新规则必须包含所有旧字段(否则未列出的字段失去验证);3. 建议用版本控制管理 $jsonSchema 规则(如 Git),方便回滚和审计。删除验证器用空对象 { } + validationLevel: 'off',而非删除 validator 字段。
Schema 版本管理实践:$jsonSchema 定义应纳入版本控制——1. 每个集合一个 JSON 文件(如 validators/users.json、validators/orders.json),与应用代码同仓库管理;2. 部署脚本按依赖顺序执行 collMod(先 users 后 orders,因为 orders 引用 users._id);3. 版本号标注:每个 Schema 文件头部注释版本号和变更日期,方便追踪;4. 回滚方案:Git revert Schema 文件 + 重新执行部署脚本即可回滚;5. CI/CD 集成:部署流水线中自动执行 Schema 迁移脚本,确保代码和 Schema 同步更新。这套实践消除了"代码改了但数据库 Schema 没改"的经典部署问题。
Schema 迁移的灰度发布策略:Schema 变更应像代码一样灰度发布——1. 阶段 1:新增字段为可选(moderate + warn),观察 1-2 周确认无异常;2. 阶段 2:迁移脚本回填历史数据(bulkWrite + $set),确保所有文档都有新字段;3. 阶段 3:新字段设为 required(strict + error),强制新文档必须包含;4. 阶段 4:应用代码开始使用新字段(之前是"有则用之,无则忽略")。每个阶段独立发布,出问题只回滚当前阶段。切忌一次完成所有变更——"先加字段、再回填、再设 required"看似低效,但每步可验证可回滚,是生产安全的唯一方式。
graph LR
A[分析新需求] --> B[设计新规则]
B --> C[validationLevel: moderate<br/>validationAction: warn]
C --> D[观察日志<br/>违规频率]
D --> E{违规多?}
E -->|多| F[调整规则/数据迁移]
E -->|少| G[validationAction: error]
F --> D
G --> H[validationLevel: strict<br/>(可选)]
style C fill:#fff3cd
style G fill:#d4edda
| 操作 | 命令 | 注意事项 |
|---|---|---|
| 添加验证器 | collMod + validator |
moderate 避免影响旧数据 |
| 修改规则 | collMod + 新 validator |
完整替换,非增量修改 |
| 删除验证器 | collMod + validator:{} + level:off |
临时关闭用 |
| 添加必填字段 | 新字段先 optional,再 required | 渐进式,避免阻断写入 |
Schema 演进的最佳实践:生产环境的 Schema 变更需要谨慎的渐进策略——1. 新增字段:先设 optional + default(旧文档自动填充默认值),运行一段时间确认无问题后再改为 required;2. 收紧约束:先用 validationAction: warn 观察,确认无违规后再切 error;3. 删除字段:先在应用层停止写入该字段,确认旧数据不再被读取后,再用 $unset 批量清理;4. 重命名字段:先添加新字段 + 双写(同时写新旧字段),迁移数据后再删除旧字段。每个步骤都需要回滚方案。
$jsonSchema 与 JSON Schema 的关系:MongoDB 的 $jsonSchema 基于 JSON Schema draft-4 规范,但做了裁剪——支持 bsonType(扩展了 ObjectId、Decimal128 等 BSON 类型)、required、properties、pattern、minimum/maximum、minItems/maxItems 等。不支持的关键字:$ref(不支持外部引用)、definitions(不支持复用定义)、anyOf/oneOf/allOf(不支持组合验证)。这些限制意味着 $jsonSchema 适合"结构性验证"(字段类型+范围+格式),不适合复杂的跨字段逻辑验证——后者应在 mongoose 层实现。
// === 添加验证器到现有集合 ===
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'],
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' }
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
// === 修改验证器 ===
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username', 'age'], // 新增 age 必填
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
age: { bsonType: 'int', minimum: 0 }
}
}
}
});
// === 移除验证器 ===
db.runCommand({
collMod: 'users',
validator: {},
validationLevel: 'off'
});
要点解析:
collMod的 validator 是完整替换,不是合并——每次修改必须写完整规则- 新增 required 字段时,建议先 moderate + warn,确认旧数据可接受后再 strict + error
- 删除验证器用空对象
{},不是删除 validator 字段
6. mongoose Schema vs MongoDB $jsonSchema
概念说明:mongoose Schema 和 MongoDB $jsonSchema 是两层互补的数据校验机制。mongoose 在应用层(Node.js 进程内)校验,灵活但仅对 Node.js 客户端有效;$jsonSchema 在数据库层(mongod 进程内)校验,刚性但对所有客户端生效。
对比分析:
| 维度 | mongoose Schema | MongoDB $jsonSchema |
|---|---|---|
| 执行层 | 应用层(Node.js) | 数据库层(mongod) |
| 性能 | 验证在应用进程 | 验证在数据库 |
| 灵活性 | ✅ 异步验证、自定义函数 | ❌ 仅静态规则 |
| 跨语言 | ❌ 仅 Node.js | ✅ 任何驱动都生效 |
| 复杂校验 | ✅ 任意 JS 代码 | ❌ 限于 JSON Schema |
| 嵌套校验 | ✅ 深度嵌套 + 引用 | ✅ 嵌套 properties |
| 自定义错误消息 | ✅ 按字段定制 | ❌ 通用错误信息 |
| 运行时修改 | ✅ 动态添加/删除 | ✅ collMod 在线修改 |
graph LR
A[客户端请求] --> B[mongoose Schema<br/>应用层校验]
B -->|通过| C[MongoDB $jsonSchema<br/>数据库层校验]
B -->|失败| D[❌ 应用层拒绝<br/>自定义错误消息]
C -->|通过| E[✅ 写入成功]
C -->|失败| F[❌ 数据库拒绝<br/>DocumentFailedValidation]
style B fill:#cce5ff
style C fill:#d4edda
style D fill:#f8d7da
style F fill:#f8d7da
最佳实践:
- 应用层(mongoose)+ 数据库层($jsonSchema)双重校验
- mongoose 做细粒度、动态校验(如"密码强度"、"跨字段关联校验")
- $jsonSchema 做基础结构校验(兜底),防止绕过应用层的直接写入
▶ 示例 2: mongoose + $jsonSchema 双重校验
// ShopHub:用户注册双重校验
// 1. mongoose 层:灵活校验 + 自定义消息
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required'],
match: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, 'Invalid email format']
},
username: {
type: String,
required: true,
minlength: [3, 'Username must be at least 3 characters'],
maxlength: 30,
validate: {
validator: async function(v) {
const count = await this.constructor.countDocuments({ username: v });
return count === 0;
},
message: 'Username already exists'
}
},
age: { type: Number, min: 18, max: 120 },
role: { type: String, enum: ['customer', 'admin', 'moderator'], default: 'customer' }
});
// 2. $jsonSchema 层:基础结构兜底
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'],
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
username: { bsonType: 'string', minLength: 3 },
age: { bsonType: 'int', minimum: 18 },
role: { enum: ['customer', 'admin', 'moderator'] }
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
// 3. 效果:Node.js 客户端走双重校验,其他客户端走 $jsonSchema 兜底
输出:
// mongoose 操作成功执行
// 数据库查询/更新结果
7. Schema 演进策略
概念说明:Schema 演进是 MongoDB 无模式数据库面临的核心运维挑战。虽然 MongoDB 不要求预定义 Schema,但生产数据总有隐式结构。当业务需求变化时,需要安全地修改校验规则而不阻断业务。
演进原则:
- 渐进式:先 optional 再 required,先 warn 再 error
- 兼容性:新规则兼容旧数据,不 retroactive 破坏
- 可回滚:每次变更记录旧规则,必要时可快速回退
- 数据优先:先迁移数据,再收紧规则
Schema 演进的安全操作清单:不同类型的 Schema 变更安全等级不同——1. 安全操作(可直接执行):新增可选字段、增大 maxLength、降低 minimum、新增 enum 值、新增 $jsonSchema 属性(不改变 required);2. 需谨慎(需先迁移数据):新增 required 字段、收紧 minLength/minimum、删除 enum 值、改变 bsonType;3. 高风险(需全面评估):删除字段、改变字段语义(如 age 从"年龄"改为"出生年份")、改变 required 字段类型。安全操作可以直接在生产环境执行,需谨慎操作必须先在 staging 环境验证,高风险操作需要完整的迁移计划 + 回滚方案 + 灰度发布。
Schema 演进的团队协作规范:Schema 变更涉及多个团队——1. 后端团队:定义 Schema 规则 + 编写迁移脚本;2. 前端团队:适配新字段的表单和展示;3. DBA 团队:执行 collMod + 监控数据库性能;4. QA 团队:验证迁移脚本的正确性 + 回滚方案。协作流程:1. 后端提交 Schema 变更 PR(含迁移脚本 + 回滚脚本);2. 前端同步适配 PR;3. Code Review 确认变更影响范围;4. Staging 环境执行迁移 + 验证;5. 生产环境灰度发布(先 warn 后 error);6. 监控 1-2 周确认无异常。规范化流程避免"改了 Schema 但前端不知道"的协作事故。
常见演进模式:
| 演进类型 | 风险 | 策略 |
|---|---|---|
| 添加新可选字段 | 低 | 直接添加 property(不 required) |
| 添加新必填字段 | 中 | 先 optional → 数据迁移 → 再 required |
| 修改字段类型 | 高 | 双写字段 → 迁移 → 切换 → 删旧字段 |
| 删除字段 | 中 | 先从 required 移除 → 确认无依赖 → 删 property |
| 收紧值范围 | 中 | 先 moderate + warn → 确认 → error |
(1) 添加新字段(兼容)
// ✅ 渐进式演进:默认字段为可选
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'], // 老字段仍必填
properties: {
email: { bsonType: 'string' },
username: { bsonType: 'string' },
age: { bsonType: 'int' } // 新增字段(不 required)
}
}
},
validationLevel: 'moderate'
});
(2) 修改字段类型(需数据迁移)
字段类型变更流程:
graph LR
A[1.添加新字段<br/>bsonType:新类型] --> B[2.双写<br/>应用同时写新老字段]
B --> C[3.数据迁移<br/>老字段→新字段]
C --> D[4.切换查询<br/>读新字段]
D --> E[5.删除老字段<br/>确认无依赖]
style A fill:#cce5ff
style E fill:#d4edda
// ⚠️ 修改字段类型需谨慎
// 1. 添加双写字段
db.runCommand({
collMod: 'users',
validator: { /* 新增新字段,保留老字段 */ }
});
// 2. 数据迁移脚本
db.users.find({ ageStr: { $exists: true } }).forEach(doc => {
db.users.updateOne(
{ _id: doc._id },
{ $set: { age: parseInt(doc.ageStr) }, $unset: { ageStr: '' } }
);
});
// 3. 删除老字段验证
8. 综合实战
概念说明:综合实战将 $jsonSchema 的所有核心特性整合到一个完整的产品集合定义中——包含类型校验、正则、枚举、范围、嵌套文档、数组元素校验等,展示生产级 Schema Validation 的全貌。
生产级 Schema 设计清单:
| 检查项 | 关键字 | 是否包含 |
|---|---|---|
| 文档类型 | bsonType: 'object' | ✅ |
| 必填字段 | required | ✅ |
| 字符串长度 | minLength / maxLength | ✅ |
| 正则模式 | pattern | ✅ |
| 数值范围 | minimum / maximum | ✅ |
| 枚举值 | enum | ✅ |
| 数组元素 | items | ✅ |
| 嵌套文档 | properties 嵌套 | ✅ |
| validationLevel | moderate | ✅ |
| validationAction | error | ✅ |
// === 创建产品集合(含完整 Schema Validation)===
db.createCollection('products', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['sku', 'title', 'price', 'category'],
properties: {
sku: {
bsonType: 'string',
pattern: '^[A-Z0-9-]+$',
maxLength: 50
},
title: {
bsonType: 'string',
minLength: 1,
maxLength: 200
},
price: {
bsonType: 'decimal'
},
category: {
enum: ['Electronics', 'Books', 'Clothing', 'Home']
},
stock: {
bsonType: 'int',
minimum: 0
},
tags: {
bsonType: 'array',
items: { bsonType: 'string' }
}
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
▶ 示例:MongoDB $jsonSchema 验证器实战
// 1. 创建带验证规则的集合
db.createCollection('users', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username', 'age'],
properties: {
email: {
bsonType: 'string',
pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
maxLength: 100
},
username: {
bsonType: 'string',
minLength: 3,
maxLength: 30,
pattern: '^[a-zA-Z0-9_]+$'
},
age: {
bsonType: 'int',
minimum: 18,
maximum: 120
},
role: {
enum: ['customer', 'admin', 'moderator']
}
}
}
},
validationLevel: 'moderate', // 仅验证新插入/更新
validationAction: 'error' // 拒绝非法数据
});
// 2. 测试合法数据 → 成功
db.users.insertOne({
email: 'alice@example.com',
username: 'alice_2026',
age: 28,
role: 'customer'
});
// { acknowledged: true, insertedId: ObjectId('...') }
// 3. 测试非法数据 → 被拒绝
db.users.insertOne({
email: 'invalid-email', // 邮箱格式错误
username: 'ab', // 用户名太短
age: 15 // 未满 18 岁
});
// 抛出错误:Document failed validation
// 错误信息包含字段名和失败原因
// 4. 修改验证器(添加新规则)
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username', 'age', 'phone'],
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
username: { bsonType: 'string', minLength: 3 },
age: { bsonType: 'int', minimum: 18 },
phone: { bsonType: 'string', pattern: '^\+?[0-9]{10,15}$' } // 新增
}
}
}
});
// 5. 嵌套文档验证
db.createCollection('orders', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['userId', 'items', 'total'],
properties: {
userId: { bsonType: 'objectId' },
items: {
bsonType: 'array',
minItems: 1,
items: {
bsonType: 'object',
required: ['productId', 'qty', 'price'],
properties: {
productId: { bsonType: 'objectId' },
qty: { bsonType: 'int', minimum: 1 },
price: { bsonType: 'decimal', minimum: 0 }
}
}
},
total: { bsonType: 'decimal', minimum: 0 }
}
}
}
});
// 6. 关闭验证器(如数据迁移时)
db.runCommand({
collMod: 'users',
validator: {},
validationLevel: 'off'
});
输出:合法数据成功插入,非法数据被拒绝并报告具体失败字段。validationLevel: moderate 确保已有数据不受影响。
Schema Validation 的运维策略:Schema Validation 在生产环境需要运维配合——1. 上线策略:先设 validationAction: 'warn'(只记录日志不拒绝),观察 1-2 周确认无误拦截,再切换为 error;2. 紧急回滚:准备回滚命令(db.runCommand({collMod: 'users', validator: {}, validationLevel: 'off'})),误拦截时快速关闭验证;3. 数据迁移:迁移脚本前关闭验证(validationLevel: 'off'),迁移后重新开启;4. 监控告警:监控 MongoDB 日志中的 validation failed 事件,频繁失败说明验证规则需要调整;5. 版本管理:将 $jsonSchema 定义存入版本控制(JSON 文件 + 部署脚本),与应用代码同步发布。
验证规则演进的安全路径:Schema 验证规则的修改分三类——1. 放宽规则(安全):新增可选字段、增大 maxLength、降低 minimum,不破坏现有数据;2. 收紧规则(危险):新增 required 字段、缩小 enum 范围、提高 minimum,已有数据可能不满足新规则;3. 类型变更(最危险):bsonType 从 string 改为 int 等,几乎必定导致验证失败。安全演进路径:先放宽(添加新字段为可选)→ 数据补全(脚本为已有文档填充新字段)→ 再收紧(新字段设为 required)。每步间隔 1-2 周,确保数据一致性。
❓ 常见问题
📖 小节
- $jsonSchema 验证器:定义文档结构
- validationAction:error(拒绝)/ warn(警告)
- validationLevel:strict(全部)/ moderate(仅新数据,推荐)
- mongoose Schema vs $jsonSchema 互补
- Schema 演进:渐进式添加,谨慎修改类型
📝 作业
- 基础题(⭐):为 users 集合创建 $jsonSchema 验证(email/username/age)。
- 基础题(⭐):测试 validationAction: warn vs error 的行为差异。
- 进阶题(⭐⭐):使用 collMod 修改现有集合的验证器(新增字段)。
- 进阶题(⭐⭐):mongoose + $jsonSchema 双重校验实现。
- 挑战题(⭐⭐⭐):完整产品集合的 $jsonSchema 定义(含嵌套、数组、Decimal128)。