第三章:数据模型与文档结构

最后更新: 2024-01-01 作者: MongoDB Team
页面目录

第三章:数据模型与文档结构

理解 MongoDB 的文档模型和 Schema 设计原则

3.1 文档结构

基本文档

// 简单的用户文档
{
  _id: ObjectId("507f1f77bcf86cd799439011"),
  username: "john_doe",
  email: "john@example.com",
  age: 28,
  is_active: true,
  created_at: ISODate("2024-01-01")
}

嵌套文档

// 包含嵌套对象的文档
{
  _id: ObjectId("507f1f77bcf86cd799439012"),
  username: "alice",
  profile: {
    first_name: "Alice",
    last_name: "Smith",
    address: {
      street: "123 Main St",
      city: "Beijing",
      country: "China",
      zip: "100000"
    },
    phone: {
      mobile: "13800138000",
      home: "010-12345678"
    }
  },
  preferences: {
    language: "zh-CN",
    timezone: "Asia/Shanghai"
  }
}

数组文档

// 包含数组的文档
{
  _id: ObjectId("507f1f77bcf86cd799439013"),
  username: "bob",
  tags: ["mongodb", "database", "nosql", "javascript"],
  scores: [
    { subject: "Math", score: 95 },
    { subject: "English", score: 88 },
    { subject: "Science", score: 92 }
  ],
  skills: [
    { name: "JavaScript", level: "expert" },
    { name: "Python", level: "advanced" }
  ]
}

3.2 _id 字段和 ObjectId

ObjectId 结构

ObjectId 是 MongoDB 默认的主键类型,12 字节组成:

┌─────────────────────────────────────────────────────────┐
│                    ObjectId 结构                        │
├──────────┬──────────┬──────────┬──────────┬────────────┤
│ 时间戳   │ 机器ID   │ 进程ID   │  计数器   │            │
│ 4 字节   │ 3 字节   │ 2 字节   │ 3 字节   │  共12字节   │
└──────────┴──────────┴──────────┴──────────┴────────────┘

ObjectId 操作

// 创建 ObjectId
ObjectId()
ObjectId("507f1f77bcf86cd799439011")

// 获取 ObjectId 的时间戳
ObjectId("507f1f77bcf86cd799439011").getTimestamp()
// ISODate("2024-01-01T00:00:00Z")

// 比较 ObjectId
ObjectId("507f1f77bcf86cd799439011") < ObjectId("507f1f77bcf86cd799439012")

// 自定义 _id
db.users.insertOne({
  _id: "user_001",
  username: "custom_id_user"
})

3.3 Schema 设计原则

嵌入 vs 引用

┌─────────────────────────────────────────────────────────────┐
│                    数据模型设计策略                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  嵌入 (Embedded)              引用 (Reference)              │
│  ─────────────                ──────────────               │
│  ✓ 数据相关性高               ✓ 数据独立性强                 │
│  ✓ 读多写少                   ✓ 经常单独访问                 │
│  ✓ 需要原子更新               ✓ 数据重复开销大               │
│  ✓ 文档大小有限制              ✓ 需要 JOIN 操作              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

嵌入文档场景

// ✅ 推荐嵌入:用户地址信息
{
  _id: ObjectId(),
  username: "user1",
  address: {
    street: "...",
    city: "...",
    country: "..."
  }
}

// ✅ 推荐嵌入:博客文章和评论(评论通常一起读取)
{
  _id: ObjectId(),
  title: "MongoDB 教程",
  content: "...",
  comments: [
    { author: "Alice", text: "很棒的文章" },
    { author: "Bob", text: "学到了很多" }
  ]
}

引用文档场景

// ✅ 推荐引用:博客文章和用户(用户被多个文章引用)
// 用户集合
{
  _id: ObjectId(),
  username: "alice",
  email: "alice@example.com"
}

// 文章集合(引用用户)
{
  _id: ObjectId(),
  title: "MongoDB 教程",
  author_id: ObjectId("..."),  // 引用用户
  content: "..."
}

3.4 文档验证

创建验证规则

// 为集合添加文档验证
db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["username", "email"],
      properties: {
        username: {
          bsonType: "string",
          minLength: 3,
          maxLength: 50,
          description: "用户名必须为3-50个字符的字符串"
        },
        email: {
          bsonType: "string",
          pattern: "^.+@.+\\..+$",
          description: "必须是有效的邮箱地址"
        },
        age: {
          bsonType: "number",
          minimum: 0,
          maximum: 150
        },
        status: {
          enum: ["active", "inactive", "suspended"]
        }
      }
    }
  },
  validationLevel: "moderate",
  validationAction: "warn"  // warn 或 error
})

// 查看集合的验证规则
db.getCollectionInfos({ name: "users" })

// 修改验证规则
db.runCommand({
  collMod: "users",
  validator: { ... },
  validationAction: "error"
})

3.5 集合设计最佳实践

BSON 文档大小限制

MongoDB 版本 文档大小限制
4.4+ 16 MB
2.6 - 4.2 16 MB
2.4 及以前 4 MB

命名规范

// 集合命名
// ✅ 正确
db.getCollection("users")
db.getCollection("order_items")
db.getCollection("system_events")

// ❌ 避免
db.getCollection("my collection")  // 含空格
db.getCollection("my.collection")  // 含点号
db.getCollection("")  // 空名称

// 数据库命名
use my_app
use analytics_platform
use reporting_system

字段命名规范

// ✅ 推荐:使用小写下划线
{
  user_id: ObjectId(),
  first_name: "John",
  last_name: "Doe",
  created_at: ISODate(),
  updated_at: ISODate(),
  is_active: true
}

// 避免使用以 $ 开头的字段名
// 避免使用含 . 的字段名(虽然支持但容易混淆)

3.6 灵活 Schema vs 固定 Schema

灵活 Schema

MongoDB 允许同一集合中的文档有不同结构:

// 集合中可以存储不同结构的文档
db.products.insertMany([
  { name: "Book", price: 29.99, author: "John" },
  { name: "T-Shirt", price: 19.99, size: "M", color: "blue" },
  { name: "Laptop", price: 999.99, specs: { cpu: "Intel i7", ram: "16GB" } }
])

使用 Schema 库(Mongoose 风格)

虽然 MongoDB 原生不强制 Schema,但可以使用验证器:

// 定义 Schema(使用验证器)
const userSchema = {
  $jsonSchema: {
    bsonType: "object",
    required: ["username", "email", "password"],
    properties: {
      username: { bsonType: "string", minLength: 3 },
      email: { bsonType: "string" },
      password: { bsonType: "string" },
      profile: {
        bsonType: "object",
        properties: {
          firstName: { bsonType: "string" },
          lastName: { bsonType: "string" }
        }
      }
    }
  }
}

db.createCollection("users", { validator: userSchema })

3.7 常见数据模型示例

博客系统

// 用户
{
  _id: ObjectId(),
  username: "alice",
  email: "alice@example.com",
  password_hash: "...",
  profile: {
    avatar: "...",
    bio: "Software Engineer",
    social_links: {
      github: "...",
      twitter: "..."
    }
  },
  created_at: ISODate()
}

// 文章
{
  _id: ObjectId(),
  author_id: ObjectId(),
  title: "MongoDB 最佳实践",
  slug: "mongodb-best-practices",
  content: "...",
  tags: ["mongodb", "database", "nosql"],
  status: "published",
  view_count: 1000,
  published_at: ISODate(),
  updated_at: ISODate()
}

// 评论
{
  _id: ObjectId(),
  post_id: ObjectId(),
  author_id: ObjectId(),
  parent_id: null,  // 用于嵌套评论
  content: "很棒的文章!",
  created_at: ISODate()
}

电商系统

// 商品
{
  _id: ObjectId(),
  name: "iPhone 15 Pro",
  category: "Electronics",
  subcategory: "Mobile Phones",
  price: NumberDecimal("7999.00"),
  stock: 100,
  attributes: {
    color: ["Black", "White", "Blue"],
    storage: ["128GB", "256GB", "512GB"],
    weight: "187g"
  },
  images: [
    { url: "...", is_primary: true },
    { url: "...", is_primary: false }
  ],
  reviews: [
    {
      user_id: ObjectId(),
      rating: 5,
      comment: "非常好用!",
      helpful_count: 10
    }
  ]
}

// 订单
{
  _id: ObjectId(),
  order_number: "ORD202401010001",
  user_id: ObjectId(),
  items: [
    {
      product_id: ObjectId(),
      name: "iPhone 15 Pro",
      quantity: 1,
      price: NumberDecimal("7999.00"),
      subtotal: NumberDecimal("7999.00")
    }
  ],
  shipping_address: {
    recipient: "张三",
    phone: "13800138000",
    address: {
      province: "北京",
      city: "北京市",
      district: "朝阳区",
      detail: "某街道某号"
    }
  },
  total_amount: NumberDecimal("7999.00"),
  status: "shipped",
  created_at: ISODate()
}

💡 实践提示

  1. 避免文档过大:16MB 限制听起来很大,但设计时应避免存储大对象
  2. 合理使用嵌套:嵌套深度建议不超过 2-3 层
  3. 考虑读写模式:读多写少适合嵌入,写多读少或需要原子更新时考虑引用
  4. 预留扩展字段:为未来可能添加的字段预留位置

📚 继续学习