MongoDB 数据建模
数据建模是 MongoDB 应用开发的关键环节。良好的数据模型可以提高查询性能、简化应用程序逻辑,并确保数据一致性。
数据建模原则
1. 根据应用程序需求建模
- 了解应用程序的数据访问模式
- 识别频繁查询的数据
- 确定读写比例
2. 优先考虑查询性能
- 将经常一起查询的数据放在一起
- 避免过多的文档引用
- 合理使用索引
3. 平衡灵活性和一致性
- 嵌入式文档提高查询性能
- 引用式文档保证数据一致性
关系建模策略
一对一关系
嵌入方式(推荐)
javascript
// 用户和用户详情一对一
{
"_id": ObjectId("..."),
"username": "zhangsan",
"email": "zhangsan@example.com",
"profile": {
"firstName": "张",
"lastName": "三",
"birthDate": ISODate("1990-01-01"),
"phone": "13800138000"
}
}引用方式
javascript
// users 集合
{ "_id": 1, "username": "zhangsan", "profile_id": 101 }
// profiles 集合
{ "_id": 101, "firstName": "张", "lastName": "三" }一对多关系
一对少(嵌入)
javascript
// 用户和地址(用户通常有少量地址)
{
"_id": ObjectId("..."),
"username": "zhangsan",
"addresses": [
{
"type": "home",
"city": "北京",
"street": "长安街1号"
},
{
"type": "work",
"city": "上海",
"street": "浦东大道100号"
}
]
}一对多(引用)
javascript
// 作者和书籍(一个作者有很多书)
// authors 集合
{
"_id": ObjectId("..."),
"name": "余华",
"nationality": "中国"
}
// books 集合
{
"_id": ObjectId("..."),
"title": "活着",
"author_id": ObjectId("..."), // 引用作者
"publishYear": 1993
}一对海量(父引用)
javascript
// 设备和日志(一个设备有大量日志)
// devices 集合
{
"_id": ObjectId("..."),
"deviceName": "传感器A001",
"location": "车间1"
}
// logs 集合
{
"_id": ObjectId("..."),
"device_id": ObjectId("..."), // 引用设备
"timestamp": ISODate("2024-01-20T10:00:00Z"),
"temperature": 25.5,
"humidity": 60
}多对多关系
双向嵌入(少量数据)
javascript
// 学生和课程(数量较少时)
// students 集合
{
"_id": ObjectId("..."),
"name": "张三",
"courses": [
{ "course_id": ObjectId("..."), "name": "数学" },
{ "course_id": ObjectId("..."), "name": "英语" }
]
}
// courses 集合
{
"_id": ObjectId("..."),
"name": "数学",
"students": [
{ "student_id": ObjectId("..."), "name": "张三" },
{ "student_id": ObjectId("..."), "name": "李四" }
]
}连接表方式(推荐)
javascript
// students 集合
{ "_id": 1, "name": "张三" }
// courses 集合
{ "_id": 101, "name": "数学" }
// enrollments 集合(连接表)
{
"_id": ObjectId("..."),
"student_id": 1,
"course_id": 101,
"enrollmentDate": ISODate("2024-01-15"),
"grade": 85
}实际建模案例
电商系统
商品集合
javascript
{
"_id": ObjectId("..."),
"sku": "PHONE-001",
"name": "iPhone 15 Pro",
"category": {
"id": ObjectId("..."),
"name": "手机"
},
"price": 8999,
"specifications": {
"color": "深空黑",
"storage": "256GB",
"screen": "6.1英寸"
},
"inventory": {
"quantity": 100,
"warehouse": "北京仓"
},
"reviews": [
{
"user_id": ObjectId("..."),
"rating": 5,
"comment": "非常好用!",
"createdAt": ISODate("2024-01-20")
}
],
"createdAt": ISODate("2024-01-01")
}订单集合
javascript
{
"_id": ObjectId("..."),
"orderNo": "ORD202401200001",
"customer": {
"user_id": ObjectId("..."),
"name": "张三",
"phone": "13800138000"
},
"items": [
{
"product_id": ObjectId("..."),
"sku": "PHONE-001",
"name": "iPhone 15 Pro",
"price": 8999,
"quantity": 1
}
],
"shipping": {
"address": "北京市朝阳区...",
"status": "已发货",
"trackingNo": "SF123456789"
},
"payment": {
"method": "支付宝",
"amount": 8999,
"status": "已支付"
},
"status": "completed",
"createdAt": ISODate("2024-01-20T10:00:00Z")
}博客系统
文章集合
javascript
{
"_id": ObjectId("..."),
"title": "MongoDB 数据建模指南",
"slug": "mongodb-data-modeling-guide",
"content": "文章内容...",
"author": {
"user_id": ObjectId("..."),
"name": "技术博主",
"avatar": "https://..."
},
"tags": ["MongoDB", "数据库", "NoSQL"],
"category": "技术",
"status": "published",
"views": 1250,
"likes": 89,
"comments_count": 15,
"publishedAt": ISODate("2024-01-20T08:00:00Z"),
"updatedAt": ISODate("2024-01-20T10:00:00Z")
}评论集合(单独存储,因为可能很多)
javascript
{
"_id": ObjectId("..."),
"article_id": ObjectId("..."),
"user": {
"user_id": ObjectId("..."),
"name": "读者A",
"avatar": "https://..."
},
"content": "写得很好!",
"parent_id": null, // 回复的评论ID,null表示顶级评论
"likes": 5,
"createdAt": ISODate("2024-01-20T09:00:00Z")
}反规范化设计
什么是反规范化
在 MongoDB 中,适当的数据冗余可以提高查询性能,避免频繁的关联查询。
反规范化场景
1. 频繁读取的关联数据
javascript
// 订单中嵌入商品基本信息,避免查询商品集合
{
"items": [
{
"product_id": ObjectId("..."),
"name": "iPhone 15 Pro", // 冗余存储
"price": 8999 // 冗余存储(历史价格)
}
]
}2. 计算字段
javascript
// 存储评论数量,避免实时计算
{
"title": "文章标题",
"content": "文章内容",
"comments_count": 15, // 冗余字段
"views": 1250 // 冗余字段
}维护反规范化数据
javascript
// 使用 $inc 原子更新计数器
db.articles.updateOne(
{ _id: articleId },
{ $inc: { comments_count: 1 } }
)索引设计
索引设计原则
- 为经常查询的字段创建索引
- 为排序字段创建索引
- 复合索引遵循 ESR 原则(Equality, Sort, Range)
示例
javascript
// 用户集合索引
db.users.createIndex({ "email": 1 }, { unique: true }) // 唯一索引
db.users.createIndex({ "username": 1 }) // 单字段索引
db.users.createIndex({ "createdAt": -1 }) // 时间倒序索引
// 订单集合索引
db.orders.createIndex({ "customer.user_id": 1, "createdAt": -1 }) // 复合索引
db.orders.createIndex({ "orderNo": 1 }, { unique: true }) // 订单号唯一总结
嵌入 vs 引用决策树
数据关系类型?
├── 一对一 → 嵌入
├── 一对少 → 嵌入
├── 一对多 → 引用(子文档ID数组)或 父引用
└── 多对多 → 连接表
查询模式?
├── 经常一起查询 → 嵌入
└── 独立查询 → 引用
数据更新频率?
├── 很少更新 → 可以嵌入(反规范化)
└── 频繁更新 → 引用(避免多处更新)最佳实践
- 优先考虑嵌入,除非有明确的理由使用引用
- 避免过深的文档嵌套(建议不超过 3 层)
- 注意 16MB 文档大小限制
- 为常用查询创建合适的索引
- 适当使用反规范化提高读性能
在下一章中,我们将学习 MongoDB 用户管理。