文档建模
MongoDB 建模的中心问题不是“有哪些实体”,而是“哪些数据需要在同一次读写中作为一个整体”。文档边界同时影响原子性、查询次数、索引、文档增长和分片后的数据局部性。
嵌入与引用
| 判断维度 | 更适合嵌入 | 更适合引用 |
|---|---|---|
| 生命周期 | 子数据随主对象创建和删除 | 对象可以独立存在 |
| 读取方式 | 经常随主对象一起读取 | 经常独立查询或被多方使用 |
| 数量 | 有明确上限 | 数量可能持续无界增长 |
| 更新 | 通常随主对象一起修改 | 高频独立更新或多方并发更新 |
| 一致性 | 需要单文档原子修改 | 可以接受跨文档协调或事务 |
| 复用 | 只属于一个聚合根 | 被多个对象共享引用 |
嵌入不是越多越好。无界评论、事件、历史记录或成员数组会使文档持续膨胀,带来移动、写冲突、网络传输和大小上限风险。
冗余与一致性
文档模型常通过复制少量字段减少运行时关联。例如订单中保存下单时的商品名称和价格,是业务快照;用户昵称的冗余则可能需要在变更后同步。
设计冗余字段时应记录:
- 数据源是谁,副本字段允许多长延迟。
- 更新失败如何补偿,是否需要 Change Stream 或任务修复。
- 字段是历史快照还是应与源数据保持一致。
- 查询收益是否值得额外写放大。
常见模型模式
| 模式 | 做法 | 适用问题 |
|---|---|---|
| Attribute | 把不固定属性转为 {key,value} 数组 | 商品规格差异大且需要统一检索 |
| Bucket | 将大量细粒度记录按时间或数量装入桶文档 | 时序、事件和测量数据 |
| Subset | 主文档只嵌入最常用的一小部分子数据 | 完整集合很大但首页只读热点子集 |
| Extended Reference | 在引用旁冗余目标对象的常用字段 | 减少高频 $lookup |
| Computed | 预先维护聚合结果 | 读远多于写的统计值 |
| Versioning | 在文档中保存 Schema Version | 渐进迁移不同结构版本 |
这些模式是权衡工具,不是必须套用的模板。采用前需要写清查询、更新、数据量和一致性要求。
Schema Validation
灵活 Schema 允许渐进演进,但生产数据仍应约束关键字段的类型、必填性和取值范围:
javascript
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["username", "enabled", "createdAt"],
properties: {
username: { bsonType: "string", minLength: 1 },
enabled: { bsonType: "bool" },
balance: { bsonType: "decimal" },
createdAt: { bsonType: "date" }
}
}
},
validationLevel: "strict",
validationAction: "error"
})Validation 不会自动把旧数据转换成新结构。规则上线前应先扫描历史数据,决定拒绝、告警、迁移或兼容多个 Schema Version。
建模评审问题
- 最常见的查询是否可以用一个文档或少量索引完成。
- 单文档是否可能持续增长到不可控。
- 哪些写入必须原子,哪些可以最终一致。
- 数组是否会成为并发更新热点或产生大量 Multikey Index Entry。
- 未来 Shard Key 是否能从模型中稳定提取。
- 字段类型和缺失、
null的语义是否一致。