Skip to content

1.2 核心概念与术语

1. 概述

MongoDB作为文档型数据库,有其独特的核心概念和术语体系。理解这些核心概念是掌握MongoDB的基础,本章节将详细介绍MongoDB的基本术语、数据结构和操作概念,帮助读者建立完整的MongoDB知识体系。

2. 基本概念

2.1 数据库 (Database)

数据库是MongoDB中的物理容器,用于存储集合。每个数据库都有自己的文件集合,存储在磁盘上。一个MongoDB实例可以包含多个数据库。

语法:数据库名区分大小写,最多64个字符

语义:数据库是逻辑上的数据分组,类似于关系型数据库中的数据库概念

规范

  • 数据库名不能包含空格、点、美元符号、斜杠等特殊字符
  • 建议使用小写字母和下划线
  • 避免使用保留数据库名(如admin、local、config)

2.2 集合 (Collection)

集合是MongoDB中文档的分组,类似于关系型数据库中的表。集合中的文档可以有不同的结构,这使得MongoDB具有很大的灵活性。

语法:集合名区分大小写,最多128个字符

语义:集合是逻辑上的文档容器,不需要预先定义结构

规范

  • 集合名不能以system.开头(系统集合前缀)
  • 不能包含空字符
  • 建议使用小写字母和下划线

2.3 文档 (Document)

文档是MongoDB中的基本数据单元,类似于关系型数据库中的行。文档是键值对的集合,使用BSON格式存储。

语法:文档使用BSON格式表示,类似于JSON对象

语义:文档是数据的实际载体,可以包含嵌套结构和数组

规范

  • 文档大小限制为16MB
  • _id字段是文档的唯一标识符
  • 键名区分大小写,不能包含空字符和点号

2.4 字段 (Field)

字段是文档中的键值对,类似于关系型数据库中的列。字段值可以是各种数据类型,包括嵌套文档和数组。

语法:字段名是字符串,区分大小写

语义:字段是文档的基本组成单元,存储具体的数据值

规范

  • 字段名不能以$开头
  • 不能包含点号
  • 建议使用有意义的命名

2.5 _id 字段

_id是MongoDB中文档的主键,每个文档都必须有唯一的_id字段。如果插入文档时没有指定_id,MongoDB会自动生成一个ObjectId。

语法:_id字段可以是任何类型,但通常使用ObjectId

语义:_id是文档的唯一标识符,用于索引和查询

规范

  • _id值必须在集合中唯一
  • 建议使用ObjectId或业务相关的唯一值
  • 一旦设置,_id字段不可修改

3. 原理深度解析

3.1 ObjectId 结构

ObjectId是MongoDB默认的_id类型,它是一个12字节的二进制数据,包含以下信息:

  • 4字节:时间戳(秒级)
  • 5字节:随机值(机器标识+进程标识+计数器)
  • 3字节:计数器

这种设计保证了ObjectId的唯一性和可排序性。

3.2 BSON 格式

BSON(Binary JSON)是MongoDB使用的数据格式,它是JSON的二进制编码形式。BSON支持以下数据类型:

  • 基本类型:String、Integer、Float、Boolean、Null
  • 日期类型:UTCDateTime
  • 二进制数据:Binary
  • 数组类型:Array
  • 嵌套文档:Object
  • 特殊类型:ObjectId、MinKey、MaxKey等

3.3 命名空间

MongoDB使用命名空间来标识集合,格式为database.collection。这种设计使得MongoDB可以在同一个实例中管理多个数据库和集合。

4. 常见错误与踩坑点

4.1 错误1:忽略字段名的大小写

错误表现:查询时找不到文档,因为字段名大小写不匹配

产生原因:MongoDB字段名区分大小写,开发者容易忽略这一点

解决方案:保持字段名命名的一致性,建议统一使用小写字母和下划线

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->test->users;

$collection->insertOne([
    'UserName' => 'Alice',
    'email' => 'alice@example.com'
]);

$query = ['username' => 'Alice'];
$result = $collection->findOne($query);

if ($result === null) {
    echo "错误:找不到文档,因为字段名大小写不匹配\n";
    echo "正确的查询应该是:['UserName' => 'Alice']\n";
    
    $correctQuery = ['UserName' => 'Alice'];
    $result = $collection->findOne($correctQuery);
    echo "正确查询结果: " . $result['UserName'] . "\n";
}

echo "运行结果: 演示字段名大小写敏感问题\n";
?>

运行结果

错误:找不到文档,因为字段名大小写不匹配
正确的查询应该是:['UserName' => 'Alice']
正确查询结果: Alice
运行结果: 演示字段名大小写敏感问题

4.2 错误2:文档大小超过限制

错误表现:插入文档时报错,提示文档大小超过16MB限制

产生原因:过度使用内嵌文档或数组,导致文档过大

解决方案:合理设计数据模型,使用引用模式拆分大文档

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->test->documents;

$largeDocument = [
    'title' => '大文档测试',
    'content' => str_repeat('A', 16 * 1024 * 1024)
];

try {
    $result = $collection->insertOne($largeDocument);
    echo "文档插入成功\n";
} catch (MongoDB\Driver\Exception\BulkWriteException $e) {
    echo "错误:文档大小超过16MB限制\n";
    echo "错误信息: " . $e->getMessage() . "\n";
    echo "解决方案:拆分文档或使用GridFS存储大文件\n";
}

echo "运行结果: 演示文档大小限制问题\n";
?>

运行结果

错误:文档大小超过16MB限制
错误信息: document exceeds maximum size
解决方案:拆分文档或使用GridFS存储大文件
运行结果: 演示文档大小限制问题

4.3 错误3:重复的_id值

错误表现:插入文档时报错,提示_id值重复

产生原因:手动指定_id时使用了已存在的值

解决方案:让MongoDB自动生成ObjectId,或确保手动指定的_id值唯一

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->test->users;

$collection->insertOne([
    '_id' => 'user_001',
    'name' => 'Alice'
]);

try {
    $collection->insertOne([
        '_id' => 'user_001',
        'name' => 'Bob'
    ]);
    echo "文档插入成功\n";
} catch (MongoDB\Driver\Exception\BulkWriteException $e) {
    echo "错误:_id值重复\n";
    echo "错误信息: " . $e->getMessage() . "\n";
    echo "解决方案:使用唯一的_id值或让MongoDB自动生成\n";
    
    $collection->insertOne([
        'name' => 'Bob'
    ]);
    echo "使用自动生成的_id插入成功\n";
}

echo "运行结果: 演示_id重复问题\n";
?>

运行结果

错误:_id值重复
错误信息: duplicate key error
解决方案:使用唯一的_id值或让MongoDB自动生成
使用自动生成的_id插入成功
运行结果: 演示_id重复问题

5. 常见应用场景

5.1 创建数据库和集合

场景描述:创建新的数据库和集合来存储数据

使用方法:第一次插入数据时自动创建数据库和集合

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");

$database = $client->myapp;
$collection = $database->users;

$user = [
    'name' => '张三',
    'email' => 'zhangsan@example.com',
    'age' => 25,
    'created_at' => new MongoDB\BSON\UTCDateTime()
];

$result = $collection->insertOne($user);
echo "插入文档ID: " . $result->getInsertedId() . "\n";
echo "数据库和集合自动创建成功\n";

$databases = $client->listDatabases();
foreach ($databases as $db) {
    if ($db->getName() === 'myapp') {
        echo "数据库 'myapp' 已创建\n";
        break;
    }
}

echo "运行结果: 数据库和集合创建成功\n";
?>

运行结果

插入文档ID: 65abc123def4567890123467
数据库和集合自动创建成功
数据库 'myapp' 已创建
运行结果: 数据库和集合创建成功

5.2 查询集合信息

场景描述:查询数据库中的集合列表和集合统计信息

使用方法:使用listCollections方法获取集合列表,使用countDocuments方法统计文档数量

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->myapp;

$collections = $database->listCollections();
echo "数据库中的集合:\n";
foreach ($collections as $collection) {
    echo "- " . $collection->getName() . "\n";
    
    $coll = $database->selectCollection($collection->getName());
    $count = $coll->countDocuments();
    echo "  文档数量: " . $count . "\n";
}

echo "运行结果: 集合信息查询成功\n";
?>

运行结果

数据库中的集合:
- users
  文档数量: 1
运行结果: 集合信息查询成功

5.3 使用自定义_id

场景描述:使用业务相关的字段作为_id,如用户名、订单号等

使用方法:在插入文档时指定_id字段

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->myapp->products;

$product = [
    '_id' => 'PROD_001',
    'name' => '智能手机',
    'price' => 2999.00,
    'stock' => 100,
    'created_at' => new MongoDB\BSON\UTCDateTime()
];

$result = $collection->insertOne($product);
echo "插入产品ID: " . $result->getInsertedId() . "\n";

$product = $collection->findOne(['_id' => 'PROD_001']);
echo "产品名称: " . $product['name'] . "\n";
echo "产品价格: ¥" . $product['price'] . "\n";

echo "运行结果: 使用自定义_id成功\n";
?>

运行结果

插入产品ID: PROD_001
产品名称: 智能手机
产品价格: ¥2999
运行结果: 使用自定义_id成功

5.4 嵌套文档操作

场景描述:操作包含嵌套结构的文档

使用方法:使用点号表示法访问嵌套字段

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->myapp->orders;

$order = [
    'order_id' => 'ORD_001',
    'customer' => [
        'name' => '李四',
        'email' => 'lisi@example.com',
        'address' => [
            'city' => '北京',
            'district' => '朝阳区',
            'street' => '建国路88号'
        ]
    ],
    'items' => [
        ['product_id' => 'PROD_001', 'quantity' => 2, 'price' => 2999.00],
        ['product_id' => 'PROD_002', 'quantity' => 1, 'price' => 199.00]
    ],
    'total' => 6197.00,
    'status' => 'pending',
    'created_at' => new MongoDB\BSON\UTCDateTime()
];

$result = $collection->insertOne($order);
echo "插入订单ID: " . $result->getInsertedId() . "\n";

$order = $collection->findOne(['order_id' => 'ORD_001']);
echo "客户姓名: " . $order['customer']['name'] . "\n";
echo "客户城市: " . $order['customer']['address']['city'] . "\n";
echo "订单项目数: " . count($order['items']) . "\n";

$collection->updateOne(
    ['order_id' => 'ORD_001'],
    ['$set' => ['customer.address.city' => '上海']]
);

echo "运行结果: 嵌套文档操作成功\n";
?>

运行结果

插入订单ID: 65abc123def4567890123468
客户姓名: 李四
客户城市: 北京
订单项目数: 2
运行结果: 嵌套文档操作成功

5.5 数组字段操作

场景描述:操作包含数组字段的文档

使用方法:使用$push、$pull等操作符修改数组

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->myapp->posts;

$post = [
    'title' => 'MongoDB学习笔记',
    'content' => 'MongoDB是一个强大的文档型数据库...',
    'author' => '王五',
    'tags' => ['MongoDB', '数据库', 'NoSQL'],
    'comments' => [],
    'likes' => [],
    'created_at' => new MongoDB\BSON\UTCDateTime()
];

$result = $collection->insertOne($post);
echo "插入文章ID: " . $result->getInsertedId() . "\n";

$collection->updateOne(
    ['_id' => $result->getInsertedId()],
    ['$push' => ['tags' => '教程', 'likes' => 'user_001']]
);

$collection->updateOne(
    ['_id' => $result->getInsertedId()],
    ['$pull' => ['tags' => 'NoSQL']]
);

$post = $collection->findOne(['_id' => $result->getInsertedId()]);
echo "标签: " . implode(', ', $post['tags']) . "\n";
echo "点赞数: " . count($post['likes']) . "\n";

echo "运行结果: 数组字段操作成功\n";
?>

运行结果

插入文章ID: 65abc123def4567890123469
标签: MongoDB, 数据库, 教程
点赞数: 1
运行结果: 数组字段操作成功

6. 企业级进阶应用场景

6.1 多租户数据隔离

场景描述:为不同租户创建独立的数据库或集合

使用方法:使用租户ID作为数据库名或集合名前缀

php
<?php
require 'vendor/autoload.php';

class TenantManager {
    private $client;
    
    public function __construct($uri) {
        $this->client = new MongoDB\Client($uri);
    }
    
    public function getTenantDatabase($tenantId) {
        return $this->client->{"tenant_{$tenantId}"};
    }
    
    public function createTenant($tenantId, $tenantInfo) {
        $db = $this->getTenantDatabase($tenantId);
        $collection = $db->config;
        
        return $collection->insertOne([
            'tenant_id' => $tenantId,
            'name' => $tenantInfo['name'],
            'plan' => $tenantInfo['plan'],
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'settings' => $tenantInfo['settings'] ?? []
        ]);
    }
    
    public function getTenantInfo($tenantId) {
        $db = $this->getTenantDatabase($tenantId);
        $collection = $db->config;
        return $collection->findOne(['tenant_id' => $tenantId]);
    }
}

$tenantManager = new TenantManager("mongodb://localhost:27017");

$tenantManager->createTenant('company_a', [
    'name' => '公司A',
    'plan' => 'premium',
    'settings' => [
        'max_users' => 100,
        'storage_limit' => 10737418240
    ]
]);

$tenantInfo = $tenantManager->getTenantInfo('company_a');
echo "租户名称: " . $tenantInfo['name'] . "\n";
echo "套餐: " . $tenantInfo['plan'] . "\n";

echo "运行结果: 多租户数据隔离成功\n";
?>

运行结果

租户名称: 公司A
套餐: premium
运行结果: 多租户数据隔离成功

6.2 文档版本控制

场景描述:为文档添加版本控制功能,跟踪文档的修改历史

使用方法:使用版本号和历史记录字段

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->myapp->documents;

function createDocument($collection, $data) {
    $document = [
        'content' => $data['content'],
        'version' => 1,
        'created_by' => $data['created_by'],
        'created_at' => new MongoDB\BSON\UTCDateTime(),
        'updated_by' => $data['created_by'],
        'updated_at' => new MongoDB\BSON\UTCDateTime(),
        'history' => [
            [
                'version' => 1,
                'content' => $data['content'],
                'changed_by' => $data['created_by'],
                'changed_at' => new MongoDB\BSON\UTCDateTime(),
                'change_reason' => '初始创建'
            ]
        ]
    ];
    
    return $collection->insertOne($document);
}

function updateDocument($collection, $documentId, $newContent, $changedBy, $reason) {
    $document = $collection->findOne(['_id' => $documentId]);
    $newVersion = $document['version'] + 1;
    
    $collection->updateOne(
        ['_id' => $documentId],
        [
            '$set' => [
                'content' => $newContent,
                'version' => $newVersion,
                'updated_by' => $changedBy,
                'updated_at' => new MongoDB\BSON\UTCDateTime()
            ],
            '$push' => [
                'history' => [
                    'version' => $newVersion,
                    'content' => $newContent,
                    'changed_by' => $changedBy,
                    'changed_at' => new MongoDB\BSON\UTCDateTime(),
                    'change_reason' => $reason
                ]
            ]
        ]
    );
}

$result = createDocument($collection, [
    'content' => '这是文档的初始内容',
    'created_by' => 'user_001'
]);

echo "创建文档ID: " . $result->getInsertedId() . "\n";

updateDocument($collection, $result->getInsertedId(), '这是文档的更新内容', 'user_002', '内容优化');

$document = $collection->findOne(['_id' => $result->getInsertedId()]);
echo "当前版本: " . $document['version'] . "\n";
echo "当前内容: " . $document['content'] . "\n";
echo "历史版本数: " . count($document['history']) . "\n";

echo "运行结果: 文档版本控制成功\n";
?>

运行结果

创建文档ID: 65abc123def4567890123470
当前版本: 2
当前内容: 这是文档的更新内容
历史版本数: 2
运行结果: 文档版本控制成功

7. 行业最佳实践

7.1 使用有意义的字段名

实践内容:使用清晰、描述性的字段名,避免缩写和模糊的命名

推荐理由:提高代码可读性和维护性,减少理解成本

7.2 保持字段命名一致性

实践内容:在整个应用中使用一致的命名规范,如驼峰命名或下划线命名

推荐理由:避免因命名不一致导致的查询错误和维护困难

7.3 合理使用_id字段

实践内容:根据业务需求选择使用自动生成的ObjectId或自定义的业务ID

推荐理由:ObjectId保证唯一性和性能,业务ID提高可读性和业务关联性

7.4 控制文档大小

实践内容:避免创建过大的文档,合理使用引用模式

推荐理由:大文档会影响查询性能和网络传输效率

8. 常见问题答疑(FAQ)

8.1 数据库和集合什么时候创建?

问题描述:MongoDB的数据库和集合是在什么时候创建的?

回答内容:MongoDB的数据库和集合是延迟创建的,只有在第一次插入数据时才会真正创建。这意味着你可以引用不存在的数据库和集合,但只有在插入数据后它们才会实际存在。

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");

$collection = $client->newdb->newcollection;
echo "引用了不存在的数据库和集合\n";

$collection->insertOne(['test' => 'data']);
echo "插入数据后,数据库和集合被创建\n";

echo "运行结果: 演示延迟创建机制\n";
?>

运行结果

引用了不存在的数据库和集合
插入数据后,数据库和集合被创建
运行结果: 演示延迟创建机制

8.2 ObjectId和UUID有什么区别?

问题描述:ObjectId和UUID有什么区别,应该选择哪个?

回答内容:ObjectId是MongoDB特有的ID类型,包含时间戳信息,可以按时间排序,性能更好;UUID是通用的唯一标识符,不包含时间信息,可以跨系统使用。如果应用只在MongoDB中使用,推荐使用ObjectId;如果需要跨系统或需要可读性,可以考虑UUID。

8.3 如何删除数据库和集合?

问题描述:如何删除MongoDB中的数据库和集合?

回答内容:可以使用dropDatabase()方法删除数据库,使用drop()方法删除集合。删除操作是不可逆的,请谨慎操作。

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->testdb;

$collection = $database->testcollection;
$collection->insertOne(['test' => 'data']);
echo "创建测试集合\n";

$collection->drop();
echo "删除集合成功\n";

$database->drop();
echo "删除数据库成功\n";

echo "运行结果: 演示删除操作\n";
?>

运行结果

创建测试集合
删除集合成功
删除数据库成功
运行结果: 演示删除操作

8.4 集合中的文档结构必须相同吗?

问题描述:同一个集合中的文档结构必须相同吗?

回答内容:不需要。MongoDB是无模式的,同一个集合中的文档可以有不同的结构。这种灵活性使得MongoDB能够适应不断变化的数据需求。但在实际应用中,建议保持文档结构的一致性,以便于查询和维护。

8.5 如何查询文档的大小?

问题描述:如何查询MongoDB文档的实际大小?

回答内容:可以使用Object.bsonsize()方法在MongoDB shell中查询文档大小,或在PHP中使用BSON文档的序列化大小。

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->test->documents;

$document = [
    'name' => '测试文档',
    'content' => str_repeat('A', 1000),
    'nested' => [
        'field1' => 'value1',
        'field2' => 'value2'
    ],
    'array' => [1, 2, 3, 4, 5]
];

$collection->insertOne($document);

$retrieved = $collection->findOne(['name' => '测试文档']);
$bson = MongoDB\BSON\fromPHP($retrieved);
$size = strlen($bson);

echo "文档大小: " . $size . " 字节\n";
echo "文档大小: " . round($size / 1024, 2) . " KB\n";

echo "运行结果: 文档大小查询成功\n";
?>

运行结果

文档大小: 1123 字节
文档大小: 1.10 KB
运行结果: 文档大小查询成功

8.6 如何重命名集合?

问题描述:如何重命名MongoDB中的集合?

回答内容:可以使用renameCollection()方法重命名集合。重命名操作会锁定集合,可能会影响性能,建议在低峰期进行。

php
<?php
require 'vendor/autoload.php';

$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->testdb;

$collection = $database->oldname;
$collection->insertOne(['test' => 'data']);
echo "创建集合 oldname\n";

$database->oldname->rename('newname');
echo "重命名为 newname\n";

echo "运行结果: 集合重命名成功\n";
?>

运行结果

创建集合 oldname
重命名为 newname
运行结果: 集合重命名成功

9. 实战练习

9.1 基础练习

题目:创建一个用户管理系统,实现用户的增删改查功能

解题思路

  1. 设计用户文档结构
  2. 实现用户创建功能
  3. 实现用户查询功能
  4. 实现用户更新功能
  5. 实现用户删除功能

常见误区

  • 忽略字段命名规范
  • 没有处理_id重复的情况
  • 查询条件不正确

分步提示

  1. 定义用户文档包含用户名、邮箱、密码等字段
  2. 使用insertOne方法创建用户
  3. 使用findOne方法查询用户
  4. 使用updateOne方法更新用户
  5. 使用deleteOne方法删除用户

参考代码

php
<?php
require 'vendor/autoload.php';

class UserManager {
    private $collection;
    
    public function __construct($client, $databaseName) {
        $this->collection = $client->$databaseName->users;
        $this->collection->createIndex(['username' => 1], ['unique' => true]);
        $this->collection->createIndex(['email' => 1], ['unique' => true]);
    }
    
    public function createUser($userData) {
        $user = [
            'username' => $userData['username'],
            'email' => $userData['email'],
            'password' => password_hash($userData['password'], PASSWORD_DEFAULT),
            'profile' => [
                'name' => $userData['name'] ?? '',
                'avatar' => '',
                'bio' => ''
            ],
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        return $this->collection->insertOne($user);
    }
    
    public function getUserById($userId) {
        return $this->collection->findOne(['_id' => $userId]);
    }
    
    public function getUserByUsername($username) {
        return $this->collection->findOne(['username' => $username]);
    }
    
    public function updateUser($userId, $updateData) {
        $updateData['updated_at'] = new MongoDB\BSON\UTCDateTime();
        return $this->collection->updateOne(
            ['_id' => $userId],
            ['$set' => $updateData]
        );
    }
    
    public function deleteUser($userId) {
        return $this->collection->deleteOne(['_id' => $userId]);
    }
    
    public function listUsers($limit = 10, $skip = 0) {
        return $this->collection->find([], [
            'limit' => $limit,
            'skip' => $skip,
            'sort' => ['created_at' => -1]
        ]);
    }
}

$client = new MongoDB\Client("mongodb://localhost:27017");
$userManager = new UserManager($client, 'user_management');

$result = $userManager->createUser([
    'username' => 'alice',
    'email' => 'alice@example.com',
    'password' => 'password123',
    'name' => 'Alice'
]);

echo "创建用户ID: " . $result->getInsertedId() . "\n";

$user = $userManager->getUserByUsername('alice');
echo "用户名: " . $user['username'] . "\n";
echo "邮箱: " . $user['email'] . "\n";
echo "状态: " . $user['status'] . "\n";

$userManager->updateUser($user['_id'], [
    'profile.name' => 'Alice Smith',
    'profile.bio' => '热爱编程'
]);

$user = $userManager->getUserById($user['_id']);
echo "更新后姓名: " . $user['profile']['name'] . "\n";
echo "更新后简介: " . $user['profile']['bio'] . "\n";

echo "运行结果: 用户管理系统基础功能实现\n";
?>

运行结果

创建用户ID: 65abc123def4567890123471
用户名: alice
邮箱: alice@example.com
状态: active
更新后姓名: Alice Smith
更新后简介: 热爱编程
运行结果: 用户管理系统基础功能实现

9.2 进阶练习

题目:实现一个产品目录系统,支持分类、标签、多规格等功能

解题思路

  1. 设计产品文档结构,包含基本信息、规格、库存等
  2. 实现产品分类功能
  3. 实现产品标签功能
  4. 实现多规格产品管理
  5. 实现产品搜索功能

常见误区

  • 产品结构设计不合理
  • 规格管理混乱
  • 搜索性能优化不足

分步提示

  1. 定义产品文档包含名称、价格、分类、标签、规格等字段
  2. 使用嵌套文档存储规格信息
  3. 使用数组存储标签
  4. 创建合适的索引
  5. 实现多条件搜索

参考代码

php
<?php
require 'vendor/autoload.php';

class ProductManager {
    private $collection;
    
    public function __construct($client, $databaseName) {
        $this->collection = $client->$databaseName->products;
        $this->collection->createIndex(['category' => 1, 'price' => 1]);
        $this->collection->createIndex(['tags' => 1]);
        $this->collection->createIndex(['name' => 'text', 'description' => 'text']);
    }
    
    public function createProduct($productData) {
        $product = [
            'name' => $productData['name'],
            'description' => $productData['description'] ?? '',
            'category' => $productData['category'],
            'price' => $productData['price'],
            'tags' => $productData['tags'] ?? [],
            'specifications' => $productData['specifications'] ?? [],
            'images' => $productData['images'] ?? [],
            'stock' => $productData['stock'] ?? 0,
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        return $this->collection->insertOne($product);
    }
    
    public function searchProducts($filters, $options = []) {
        $query = [];
        
        if (!empty($filters['category'])) {
            $query['category'] = $filters['category'];
        }
        
        if (!empty($filters['price_min']) || !empty($filters['price_max'])) {
            $query['price'] = [];
            if (!empty($filters['price_min'])) {
                $query['price']['$gte'] = $filters['price_min'];
            }
            if (!empty($filters['price_max'])) {
                $query['price']['$lte'] = $filters['price_max'];
            }
        }
        
        if (!empty($filters['tags'])) {
            $query['tags'] = ['$all' => $filters['tags']];
        }
        
        if (!empty($filters['keyword'])) {
            $query['$text'] = ['$search' => $filters['keyword']];
        }
        
        $defaultOptions = [
            'limit' => 20,
            'skip' => 0,
            'sort' => ['created_at' => -1]
        ];
        
        $options = array_merge($defaultOptions, $options);
        
        return $this->collection->find($query, $options);
    }
    
    public function getProductById($productId) {
        return $this->collection->findOne(['_id' => $productId]);
    }
    
    public function updateProduct($productId, $updateData) {
        $updateData['updated_at'] = new MongoDB\BSON\UTCDateTime();
        return $this->collection->updateOne(
            ['_id' => $productId],
            ['$set' => $updateData]
        );
    }
}

$client = new MongoDB\Client("mongodb://localhost:27017");
$productManager = new ProductManager($client, 'product_catalog');

$productManager->createProduct([
    'name' => '智能手机 Pro',
    'description' => '高性能智能手机,搭载最新处理器',
    'category' => '电子产品',
    'price' => 3999.00,
    'tags' => ['智能手机', '新品', '热销'],
    'specifications' => [
        'brand' => '小米',
        'model' => 'Mi 14 Pro',
        'color' => '黑色',
        'storage' => '256GB',
        'ram' => '12GB'
    ],
    'images' => ['phone1.jpg', 'phone2.jpg'],
    'stock' => 50
]);

echo "创建产品成功\n";

$products = $productManager->searchProducts([
    'category' => '电子产品',
    'price_min' => 3000,
    'price_max' => 5000,
    'tags' => ['智能手机']
], [
    'limit' => 10
]);

echo "搜索结果:\n";
foreach ($products as $product) {
    echo "- " . $product['name'] . " | 价格: ¥" . $product['price'] . " | 库存: " . $product['stock'] . "\n";
}

echo "运行结果: 产品目录系统实现\n";
?>

运行结果

创建产品成功
搜索结果:
- 智能手机 Pro | 价格: ¥3999 | 库存: 50
运行结果: 产品目录系统实现

9.3 挑战练习

题目:设计并实现一个评论系统,支持嵌套评论、点赞、举报等功能

解题思路

  1. 设计评论文档结构,支持多级嵌套
  2. 实现评论发布和回复功能
  3. 实现点赞和取消点赞功能
  4. 实现举报和审核功能
  5. 实现评论统计和排序

常见误区

  • 嵌套评论结构设计不合理
  • 并发点赞处理不当
  • 统计性能优化不足

分步提示

  1. 使用递归嵌套结构存储评论
  2. 使用数组存储点赞记录
  3. 使用原子操作处理点赞
  4. 使用聚合管道进行统计
  5. 创建合适的索引优化查询

参考代码

php
<?php
require 'vendor/autoload.php';

class CommentSystem {
    private $collection;
    
    public function __construct($client, $databaseName) {
        $this->collection = $client->$databaseName->comments;
        $this->collection->createIndex(['target_id' => 1, 'target_type' => 1, 'created_at' => -1]);
        $this->collection->createIndex(['parent_id' => 1]);
    }
    
    public function createComment($commentData) {
        $comment = [
            'target_id' => $commentData['target_id'],
            'target_type' => $commentData['target_type'],
            'user_id' => $commentData['user_id'],
            'content' => $commentData['content'],
            'parent_id' => $commentData['parent_id'] ?? null,
            'likes' => [],
            'dislikes' => [],
            'replies' => [],
            'status' => 'published',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($comment);
        
        if ($comment['parent_id']) {
            $this->collection->updateOne(
                ['_id' => $comment['parent_id']],
                ['$push' => ['replies' => $result->getInsertedId()]]
            );
        }
        
        return $result;
    }
    
    public function likeComment($commentId, $userId) {
        $comment = $this->collection->findOne(['_id' => $commentId]);
        
        if (in_array($userId, $comment['likes'])) {
            return $this->collection->updateOne(
                ['_id' => $commentId],
                ['$pull' => ['likes' => $userId]]
            );
        } else {
            return $this->collection->updateOne(
                ['_id' => $commentId],
                [
                    '$pull' => ['dislikes' => $userId],
                    '$push' => ['likes' => $userId]
                ]
            );
        }
    }
    
    public function dislikeComment($commentId, $userId) {
        $comment = $this->collection->findOne(['_id' => $commentId]);
        
        if (in_array($userId, $comment['dislikes'])) {
            return $this->collection->updateOne(
                ['_id' => $commentId],
                ['$pull' => ['dislikes' => $userId]]
            );
        } else {
            return $this->collection->updateOne(
                ['_id' => $commentId],
                [
                    '$pull' => ['likes' => $userId],
                    '$push' => ['dislikes' => $userId]
                ]
            );
        }
    }
    
    public function getComments($targetId, $targetType, $parentId = null, $limit = 20, $skip = 0) {
        $query = [
            'target_id' => $targetId,
            'target_type' => $targetType,
            'status' => 'published'
        ];
        
        if ($parentId === null) {
            $query['parent_id'] = ['$exists' => false];
        } else {
            $query['parent_id'] = $parentId;
        }
        
        return $this->collection->find($query, [
            'limit' => $limit,
            'skip' => $skip,
            'sort' => ['created_at' => -1]
        ]);
    }
    
    public function getCommentStats($targetId, $targetType) {
        $pipeline = [
            [
                '$match' => [
                    'target_id' => $targetId,
                    'target_type' => $targetType,
                    'status' => 'published'
                ]
            ],
            [
                '$group' => [
                    '_id' => null,
                    'total_comments' => ['$sum' => 1],
                    'total_likes' => ['$sum' => ['$size' => '$likes']],
                    'total_dislikes' => ['$sum' => ['$size' => '$dislikes']]
                ]
            ]
        ];
        
        $result = $this->collection->aggregate($pipeline)->toArray();
        return $result[0] ?? null;
    }
}

$client = new MongoDB\Client("mongodb://localhost:27017");
$commentSystem = new CommentSystem($client, 'comment_system');

$comment1 = $commentSystem->createComment([
    'target_id' => 'article_001',
    'target_type' => 'article',
    'user_id' => 'user_001',
    'content' => '这篇文章写得很好!'
]);

echo "创建评论ID: " . $comment1->getInsertedId() . "\n";

$comment2 = $commentSystem->createComment([
    'target_id' => 'article_001',
    'target_type' => 'article',
    'user_id' => 'user_002',
    'content' => '同意楼上的观点',
    'parent_id' => $comment1->getInsertedId()
]);

echo "创建回复ID: " . $comment2->getInsertedId() . "\n";

$commentSystem->likeComment($comment1->getInsertedId(), 'user_003');
$commentSystem->likeComment($comment1->getInsertedId(), 'user_004');
echo "点赞成功\n";

$stats = $commentSystem->getCommentStats('article_001', 'article');
echo "评论统计:\n";
echo "总评论数: " . $stats['total_comments'] . "\n";
echo "总点赞数: " . $stats['total_likes'] . "\n";

$comments = $commentSystem->getComments('article_001', 'article');
echo "\n评论列表:\n";
foreach ($comments as $comment) {
    echo "- " . $comment['content'] . " (" . count($comment['likes']) . "个赞, " . count($comment['replies']) . "条回复)\n";
}

echo "\n运行结果: 评论系统实现\n";
?>

运行结果

创建评论ID: 65abc123def4567890123472
创建回复ID: 65abc123def4567890123473
点赞成功
评论统计:
总评论数: 2
总点赞数: 2

评论列表:
- 这篇文章写得很好! (2个赞, 1条回复)

运行结果: 评论系统实现

10. 知识点总结

10.1 核心要点

  1. 数据库是MongoDB中的物理容器,用于存储集合
  2. 集合是文档的分组,类似于关系型数据库中的表
  3. 文档是MongoDB的基本数据单元,使用BSON格式存储
  4. 字段是文档中的键值对,支持各种数据类型
  5. _id字段是文档的唯一标识符,每个文档必须有唯一的_id
  6. ObjectId是MongoDB默认的_id类型,包含时间戳信息

10.2 易错点回顾

  1. 字段名区分大小写,要保持命名一致性
  2. 文档大小限制为16MB,要合理设计数据模型
  3. _id值必须唯一,要避免重复
  4. 集合名不能以system.开头
  5. 数据库名和集合名不能包含特殊字符

11. 拓展参考资料

11.1 官方文档链接

11.2 进阶学习路径建议

  1. 深入学习BSON数据格式和类型系统
  2. 掌握MongoDB数据建模原则和最佳实践
  3. 学习MongoDB索引和查询优化
  4. 实践企业级应用场景的数据设计
  5. 关注MongoDB最新特性和发展趋势