Skip to content

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 } }
)

索引设计

索引设计原则

  1. 为经常查询的字段创建索引
  2. 为排序字段创建索引
  3. 复合索引遵循 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数组)或 父引用
└── 多对多 → 连接表

查询模式?
├── 经常一起查询 → 嵌入
└── 独立查询 → 引用

数据更新频率?
├── 很少更新 → 可以嵌入(反规范化)
└── 频繁更新 → 引用(避免多处更新)

最佳实践

  1. 优先考虑嵌入,除非有明确的理由使用引用
  2. 避免过深的文档嵌套(建议不超过 3 层)
  3. 注意 16MB 文档大小限制
  4. 为常用查询创建合适的索引
  5. 适当使用反规范化提高读性能

在下一章中,我们将学习 MongoDB 用户管理