Appearance
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 基础练习
题目:创建一个用户管理系统,实现用户的增删改查功能
解题思路:
- 设计用户文档结构
- 实现用户创建功能
- 实现用户查询功能
- 实现用户更新功能
- 实现用户删除功能
常见误区:
- 忽略字段命名规范
- 没有处理_id重复的情况
- 查询条件不正确
分步提示:
- 定义用户文档包含用户名、邮箱、密码等字段
- 使用insertOne方法创建用户
- 使用findOne方法查询用户
- 使用updateOne方法更新用户
- 使用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 进阶练习
题目:实现一个产品目录系统,支持分类、标签、多规格等功能
解题思路:
- 设计产品文档结构,包含基本信息、规格、库存等
- 实现产品分类功能
- 实现产品标签功能
- 实现多规格产品管理
- 实现产品搜索功能
常见误区:
- 产品结构设计不合理
- 规格管理混乱
- 搜索性能优化不足
分步提示:
- 定义产品文档包含名称、价格、分类、标签、规格等字段
- 使用嵌套文档存储规格信息
- 使用数组存储标签
- 创建合适的索引
- 实现多条件搜索
参考代码:
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 挑战练习
题目:设计并实现一个评论系统,支持嵌套评论、点赞、举报等功能
解题思路:
- 设计评论文档结构,支持多级嵌套
- 实现评论发布和回复功能
- 实现点赞和取消点赞功能
- 实现举报和审核功能
- 实现评论统计和排序
常见误区:
- 嵌套评论结构设计不合理
- 并发点赞处理不当
- 统计性能优化不足
分步提示:
- 使用递归嵌套结构存储评论
- 使用数组存储点赞记录
- 使用原子操作处理点赞
- 使用聚合管道进行统计
- 创建合适的索引优化查询
参考代码:
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 核心要点
- 数据库是MongoDB中的物理容器,用于存储集合
- 集合是文档的分组,类似于关系型数据库中的表
- 文档是MongoDB的基本数据单元,使用BSON格式存储
- 字段是文档中的键值对,支持各种数据类型
- _id字段是文档的唯一标识符,每个文档必须有唯一的_id
- ObjectId是MongoDB默认的_id类型,包含时间戳信息
10.2 易错点回顾
- 字段名区分大小写,要保持命名一致性
- 文档大小限制为16MB,要合理设计数据模型
- _id值必须唯一,要避免重复
- 集合名不能以system.开头
- 数据库名和集合名不能包含特殊字符
11. 拓展参考资料
11.1 官方文档链接
- MongoDB核心概念: https://www.mongodb.com/docs/manual/core/data-modeling/
- BSON规范: https://www.mongodb.com/docs/manual/reference/bson-types/
- ObjectId规范: https://www.mongodb.com/docs/manual/reference/method/ObjectId/
11.2 进阶学习路径建议
- 深入学习BSON数据格式和类型系统
- 掌握MongoDB数据建模原则和最佳实践
- 学习MongoDB索引和查询优化
- 实践企业级应用场景的数据设计
- 关注MongoDB最新特性和发展趋势
