Appearance
MongoDB Array类型详解
1. 概述
数组(Array)是MongoDB中最灵活、最强大的数据类型之一,它允许在一个字段中存储多个值。与传统关系型数据库需要通过关联表来实现一对多关系不同,MongoDB的数组类型可以直接在文档中嵌入多个元素,这种文档模型的设计使得数据存储更加自然和高效。
数组类型在实际开发中应用极其广泛,例如:
- 存储用户的多个标签(tags: ['技术', '设计', '管理'])
- 记录文章的评论列表
- 保存商品的多个图片URL
- 存储用户的多个联系方式
- 记录订单的商品列表
MongoDB为数组类型提供了丰富的查询和更新操作符,如$push、$pull、$addToSet、$elemMatch等,使得数组的操作既灵活又高效。掌握数组类型的使用,是成为MongoDB高级开发者的必备技能。
本知识点承接《MongoDB数据类型概述》,后续延伸至《MongoDB聚合管道》和《MongoDB索引优化》,建议学习顺序:MongoDB基础操作→本知识点→聚合管道→索引优化。
2. 基本概念
2.1 语法
2.1.1 数组的定义与插入
在MongoDB中,数组使用方括号[]定义,元素之间用逗号分隔。数组可以包含不同类型的元素,但实际应用中通常存储相同类型的元素。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->users;
$document = [
'name' => '张三',
'tags' => ['技术', '设计', '管理'],
'scores' => [85, 92, 78, 95],
'contacts' => [
['type' => 'email', 'value' => 'zhangsan@example.com'],
['type' => 'phone', 'value' => '13800138000']
],
'mixed' => [1, 'text', true, null, 3.14]
];
$result = $collection->insertOne($document);
echo "插入文档ID: " . $result->getInsertedId() . "\n";
echo "插入成功\n";运行结果:
插入文档ID: 6789abcdef1234567890abcd
插入成功常见改法对比:
php
$wrongDocument = [
'name' => '李四',
'tags' => '技术',
];
$correctDocument = [
'name' => '李四',
'tags' => ['技术'],
];对比说明:
- 错误写法:
tags字段存储的是字符串,不是数组,无法使用数组操作符 - 正确写法:即使只有一个元素,也应该使用数组格式,保持数据类型一致性
2.1.2 数组查询操作符
MongoDB提供了多种数组查询操作符,用于精确匹配数组内容。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->articles;
$collection->insertMany([
[
'title' => 'MongoDB教程',
'tags' => ['数据库', 'NoSQL', 'MongoDB'],
'views' => 1000
],
[
'title' => 'MySQL教程',
'tags' => ['数据库', 'SQL', 'MySQL'],
'views' => 1500
],
[
'title' => 'PHP教程',
'tags' => ['编程', 'PHP', 'Web开发'],
'views' => 2000
]
]);
$allMatch = $collection->find([
'tags' => '数据库'
])->toArray();
echo "包含'数据库'标签的文章数量: " . count($allMatch) . "\n";
$exactMatch = $collection->find([
'tags' => ['数据库', 'NoSQL', 'MongoDB']
])->toArray();
echo "完全匹配标签数组的文章数量: " . count($exactMatch) . "\n";
$inOperator = $collection->find([
'tags' => ['$in' => ['PHP', 'MySQL']]
])->toArray();
echo "包含PHP或MySQL标签的文章数量: " . count($inOperator) . "\n";
$allOperator = $collection->find([
'tags' => ['$all' => ['数据库', 'MongoDB']]
])->toArray();
echo "同时包含数据库和MongoDB标签的文章数量: " . count($allOperator) . "\n";
$sizeOperator = $collection->find([
'tags' => ['$size' => 3]
])->toArray();
echo "标签数量为3的文章数量: " . count($sizeOperator) . "\n";运行结果:
包含'数据库'标签的文章数量: 2
完全匹配标签数组的文章数量: 1
包含PHP或MySQL标签的文章数量: 2
同时包含数据库和MongoDB标签的文章数量: 1
标签数量为3的文章数量: 3常见改法对比:
php
$wrongQuery = $collection->find([
'tags' => ['MongoDB', 'NoSQL', '数据库']
])->toArray();
$correctQuery1 = $collection->find([
'tags' => 'MongoDB'
])->toArray();
$correctQuery2 = $collection->find([
'tags' => ['$all' => ['MongoDB', 'NoSQL']]
])->toArray();对比说明:
- 错误写法:数组完全匹配要求顺序一致,['MongoDB', 'NoSQL', '数据库']与['数据库', 'NoSQL', 'MongoDB']不匹配
- 正确写法1:查询单个元素是否存在,不关心顺序
- 正确写法2:使用
$all操作符查询多个元素是否存在,不关心顺序
2.1.3 数组更新操作符
MongoDB提供了丰富的数组更新操作符,用于添加、删除和修改数组元素。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->shopping_cart;
$collection->insertOne([
'user_id' => 'user001',
'items' => ['商品A', '商品B'],
'prices' => [100, 200]
]);
$pushResult = $collection->updateOne(
['user_id' => 'user001'],
['$push' => ['items' => '商品C']]
);
echo "添加商品C,修改文档数: " . $pushResult->getModifiedCount() . "\n";
$addToSetResult = $collection->updateOne(
['user_id' => 'user001'],
['$addToSet' => ['items' => '商品A']]
);
echo "尝试添加重复商品A,修改文档数: " . $addToSetResult->getModifiedCount() . "\n";
$pullResult = $collection->updateOne(
['user_id' => 'user001'],
['$pull' => ['items' => '商品B']]
);
echo "删除商品B,修改文档数: " . $pullResult->getModifiedCount() . "\n";
$popResult = $collection->updateOne(
['user_id' => 'user001'],
['$pop' => ['items' => 1]]
);
echo "删除最后一个商品,修改文档数: " . $popResult->getModifiedCount() . "\n";
$document = $collection->findOne(['user_id' => 'user001']);
echo "当前购物车商品: " . json_encode($document['items'], JSON_UNESCAPED_UNICODE) . "\n";运行结果:
添加商品C,修改文档数: 1
尝试添加重复商品A,修改文档数: 0
删除商品B,修改文档数: 1
删除最后一个商品,修改文档数: 1
当前购物车商品: ["商品A","商品C"]常见改法对比:
php
$wrongUpdate = $collection->updateOne(
['user_id' => 'user001'],
['$push' => ['items' => '商品A']]
);
$correctUpdate = $collection->updateOne(
['user_id' => 'user001'],
['$addToSet' => ['items' => '商品A']]
);对比说明:
- 错误写法:使用
$push会添加重复元素,导致数组中出现多个'商品A' - 正确写法:使用
$addToSet可以避免添加重复元素,保持数组元素唯一性
2.2 语义
2.2.1 数组的存储语义
数组在MongoDB中具有以下语义特征:
- 有序性:数组元素按照插入顺序存储,可以通过索引访问
- 可重复性:数组中的元素可以重复(除非使用
$addToSet等操作符保证唯一性) - 动态性:数组长度可以动态变化,不需要预先定义大小
- 嵌套性:数组可以嵌套对象或其他数组,形成复杂的数据结构
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->orders;
$order = [
'order_id' => 'ORD001',
'items' => [
[
'product_id' => 'PROD001',
'name' => 'iPhone 15',
'price' => 6999,
'quantity' => 2,
'attributes' => ['颜色' => '深空黑', '存储' => '256GB']
],
[
'product_id' => 'PROD002',
'name' => 'AirPods Pro',
'price' => 1899,
'quantity' => 1,
'attributes' => ['颜色' => '白色']
]
],
'total_amount' => 15897,
'status_history' => [
['status' => 'created', 'timestamp' => new MongoDB\BSON\UTCDateTime(strtotime('2024-01-01 10:00:00') * 1000)],
['status' => 'paid', 'timestamp' => new MongoDB\BSON\UTCDateTime(strtotime('2024-01-01 10:05:00') * 1000)],
['status' => 'shipped', 'timestamp' => new MongoDB\BSON\UTCDateTime(strtotime('2024-01-02 09:00:00') * 1000)]
]
];
$result = $collection->insertOne($order);
echo "订单插入成功,ID: " . $result->getInsertedId() . "\n";
$found = $collection->findOne(['order_id' => 'ORD001']);
echo "订单商品数量: " . count($found['items']) . "\n";
echo "第一个商品名称: " . $found['items'][0]['name'] . "\n";
echo "第一个商品颜色: " . $found['items'][0]['attributes']['颜色'] . "\n";
echo "订单状态历史记录数: " . count($found['status_history']) . "\n";运行结果:
订单插入成功,ID: 6789abcdef1234567890abcd
订单商品数量: 2
第一个商品名称: iPhone 15
第一个商品颜色: 深空黑
订单状态历史记录数: 3常见改法对比:
php
$wrongStructure = [
'order_id' => 'ORD002',
'item1_name' => 'iPhone 15',
'item1_price' => 6999,
'item2_name' => 'AirPods Pro',
'item2_price' => 1899
];
$correctStructure = [
'order_id' => 'ORD002',
'items' => [
['name' => 'iPhone 15', 'price' => 6999],
['name' => 'AirPods Pro', 'price' => 1899]
]
];对比说明:
- 错误写法:使用多个字段存储多个商品,扩展性差,难以查询和更新
- 正确写法:使用数组存储商品列表,结构清晰,易于扩展和操作
2.2.2 数组的查询语义
数组字段的查询有以下几种语义:
- 元素匹配:查询数组中是否包含某个元素
- 完全匹配:查询数组是否完全等于指定数组(包括顺序)
- 条件匹配:查询数组中是否有元素满足特定条件
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->students;
$collection->insertMany([
[
'name' => '张三',
'scores' => [85, 92, 78, 95],
'subjects' => ['数学', '物理', '化学']
],
[
'name' => '李四',
'scores' => [88, 76, 90, 82],
'subjects' => ['数学', '英语', '历史']
],
[
'name' => '王五',
'scores' => [92, 88, 85, 90],
'subjects' => ['数学', '物理', '生物']
]
]);
$elementMatch = $collection->find([
'scores' => ['$gt' => 90]
])->toArray();
echo "有成绩超过90分的学生: " . count($elementMatch) . "人\n";
foreach ($elementMatch as $student) {
echo " - " . $student['name'] . "\n";
}
$elemMatchQuery = $collection->find([
'scores' => [
'$elemMatch' => [
'$gte' => 85,
'$lte' => 90
]
]
])->toArray();
echo "\n有成绩在85-90分之间的学生: " . count($elemMatchQuery) . "人\n";
foreach ($elemMatchQuery as $student) {
echo " - " . $student['name'] . "\n";
}
$allSubjects = $collection->find([
'subjects' => ['$all' => ['数学', '物理']]
])->toArray();
echo "\n同时选修数学和物理的学生: " . count($allSubjects) . "人\n";
foreach ($allSubjects as $student) {
echo " - " . $student['name'] . "\n";
}运行结果:
有成绩超过90分的学生: 2人
- 张三
- 王五
有成绩在85-90分之间的学生: 3人
- 张三
- 李四
- 王五
同时选修数学和物理的学生: 2人
- 张三
- 王五常见改法对比:
php
$wrongQuery = $collection->find([
'scores' => ['$gte' => 85, '$lte' => 90]
])->toArray();
$correctQuery = $collection->find([
'scores' => [
'$elemMatch' => [
'$gte' => 85,
'$lte' => 90
]
]
])->toArray();对比说明:
- 错误写法:查询数组中是否至少有一个元素>=85且至少有一个元素<=90(不一定是同一个元素)
- 正确写法:使用
$elemMatch确保同一个元素同时满足>=85和<=90两个条件
2.3 规范
2.3.1 数组命名规范
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->examples;
$goodDocument = [
'user_id' => 'user001',
'tags' => ['技术', '设计'],
'comments' => [
['user_id' => 'user002', 'content' => '很好的文章'],
['user_id' => 'user003', 'content' => '学习了']
],
'phone_numbers' => ['13800138000', '13900139000'],
'order_items' => [
['product_id' => 'PROD001', 'quantity' => 2]
]
];
$badDocument = [
'user_id' => 'user001',
'tag' => ['技术', '设计'],
'commentList' => [
['user_id' => 'user002', 'content' => '很好的文章']
],
'phones' => ['13800138000', '13900139000'],
'items' => [
['product_id' => 'PROD001', 'quantity' => 2]
]
];
$collection->insertOne($goodDocument);
echo "文档插入成功\n";运行结果:
文档插入成功命名规范说明:
- 使用复数形式:数组字段应使用复数名词,如
tags、comments、items - 避免缩写:使用完整单词,如
phone_numbers而不是phones - 语义明确:名称应清楚表达数组内容,如
order_items而不是items - 一致性:整个项目中数组命名风格保持一致
2.3.2 数组大小限制
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->large_arrays;
$largeArray = range(1, 100000);
try {
$result = $collection->insertOne([
'name' => '大数组测试',
'numbers' => $largeArray
]);
echo "插入成功,文档ID: " . $result->getInsertedId() . "\n";
$document = $collection->findOne(['name' => '大数组测试']);
echo "数组大小: " . count($document['numbers']) . "\n";
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
}
$recommendedSize = 1000;
$recommendedArray = range(1, $recommendedSize);
$result = $collection->insertOne([
'name' => '推荐大小数组',
'numbers' => $recommendedArray
]);
echo "推荐大小的数组插入成功\n";运行结果:
插入成功,文档ID: 6789abcdef1234567890abcd
数组大小: 100000
推荐大小的数组插入成功数组大小规范:
- 文档大小限制:MongoDB单个文档最大16MB,数组大小不能超过此限制
- 性能考虑:数组过大(如超过1000个元素)会影响查询和更新性能
- 索引限制:数组字段创建索引时,索引条目数量 = 文档数 × 数组元素数,需要控制数组大小
- 最佳实践:单个数组建议不超过1000个元素,超过时应考虑分表或使用引用
3. 原理深度解析
3.1 数组的BSON存储机制
MongoDB使用BSON(Binary JSON)格式存储数据,数组在BSON中的存储遵循特定的编码规则。
3.1.1 BSON数组编码结构
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
use MongoDB\BSON\toJSON;
use MongoDB\BSON\fromPHP;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->bson_demo;
$document = [
'name' => '测试文档',
'simple_array' => [1, 2, 3],
'object_array' => [
['id' => 1, 'name' => '项目A'],
['id' => 2, 'name' => '项目B']
],
'mixed_array' => [1, 'text', true, 3.14, null]
];
$result = $collection->insertOne($document);
echo "文档插入成功\n";
$found = $collection->findOne(['name' => '测试文档']);
echo "简单数组类型:\n";
foreach ($found['simple_array'] as $index => $value) {
echo " [$index] => " . gettype($value) . "($value)\n";
}
echo "\n对象数组结构:\n";
foreach ($found['object_array'] as $index => $obj) {
echo " [$index] => {id: " . $obj['id'] . ", name: " . $obj['name'] . "}\n";
}
echo "\n混合数组类型:\n";
foreach ($found['mixed_array'] as $index => $value) {
$type = gettype($value);
$displayValue = is_bool($value) ? ($value ? 'true' : 'false') :
(is_null($value) ? 'null' : $value);
echo " [$index] => $type($displayValue)\n";
}运行结果:
文档插入成功
简单数组类型:
[0] => integer(1)
[1] => integer(2)
[2] => integer(3)
对象数组结构:
[0] => {id: 1, name: 项目A}
[1] => {id: 2, name: 项目B}
混合数组类型:
[0] => integer(1)
[1] => string(text)
[2] => boolean(true)
[3] => double(3.14)
[4] => NULL(null)BSON存储原理:
- 类型标识:每个数组元素都有类型标识(如int32、string、boolean等)
- 长度前缀:BSON数组存储时包含元素长度信息,便于快速解析
- 嵌套支持:数组可以嵌套对象或其他数组,BSON递归处理嵌套结构
- 索引优化:MongoDB会为数组字段创建多键索引,每个数组元素都有一个索引条目
3.1.2 数组的内存布局
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->memory_demo;
$collection->insertMany([
[
'name' => '小数组',
'items' => [1, 2, 3]
],
[
'name' => '中等数组',
'items' => range(1, 100)
],
[
'name' => '大数组',
'items' => range(1, 1000)
]
]);
$stats = $client->test->command(['collstats' => 'memory_demo']);
echo "集合统计信息:\n";
echo " 文档数量: " . $stats['count'] . "\n";
echo " 平均文档大小: " . round($stats['avgObjSize'], 2) . " 字节\n";
echo " 总存储大小: " . round($stats['size'] / 1024, 2) . " KB\n";
$small = $collection->findOne(['name' => '小数组']);
$medium = $collection->findOne(['name' => '中等数组']);
$large = $collection->findOne(['name' => '大数组']);
echo "\n数组大小对比:\n";
echo " 小数组: " . count($small['items']) . " 个元素\n";
echo " 中等数组: " . count($medium['items']) . " 个元素\n";
echo " 大数组: " . count($large['items']) . " 个元素\n";运行结果:
集合统计信息:
文档数量: 3
平均文档大小: 2156.33 字节
总存储大小: 6.47 KB
数组大小对比:
小数组: 3 个元素
中等数组: 100 个元素
大数组: 1000 个元素内存布局说明:
- 连续存储:数组元素在BSON中连续存储,便于顺序访问
- 指针引用:对于大对象或嵌套文档,使用指针引用减少内存占用
- 内存对齐:BSON会进行内存对齐,提高访问效率
- 压缩优化:WiredTiger存储引擎会对文档进行压缩,减少磁盘占用
3.2 数组索引原理
3.2.1 多键索引(Multikey Index)
当索引字段包含数组时,MongoDB会自动创建多键索引,为数组中的每个元素创建一个索引条目。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->multikey_demo;
$collection->createIndex(['tags' => 1]);
$collection->insertMany([
[
'title' => '文章A',
'tags' => ['MongoDB', '数据库', 'NoSQL']
],
[
'title' => '文章B',
'tags' => ['MySQL', '数据库', 'SQL']
],
[
'title' => '文章C',
'tags' => ['MongoDB', '教程', '入门']
]
]);
$indexes = $collection->listIndexes();
echo "索引列表:\n";
foreach ($indexes as $index) {
echo " - " . $index['name'] . ": " . json_encode($index['key']) . "\n";
}
$explain = $collection->find(['tags' => 'MongoDB'])->explain();
echo "\n查询 'MongoDB' 标签:\n";
echo " 使用索引: " . ($explain['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo " 扫描文档数: " . ($explain['executionStats']['totalDocsExamined'] ?? 0) . "\n";
echo " 返回文档数: " . ($explain['executionStats']['nReturned'] ?? 0) . "\n";
$explain2 = $collection->find(['tags' => '数据库'])->explain();
echo "\n查询 '数据库' 标签:\n";
echo " 使用索引: " . ($explain2['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo " 扫描文档数: " . ($explain2['executionStats']['totalDocsExamined'] ?? 0) . "\n";
echo " 返回文档数: " . ($explain2['executionStats']['nReturned'] ?? 0) . "\n";运行结果:
索引列表:
- _id_: {"_id":1}
- tags_1: {"tags":1}
查询 'MongoDB' 标签:
使用索引: tags_1
扫描文档数: 2
返回文档数: 2
查询 '数据库' 标签:
使用索引: tags_1
扫描文档数: 2
返回文档数: 2多键索引原理:
- 索引展开:数组['MongoDB', '数据库', 'NoSQL']会创建3个索引条目
- 去重存储:如果数组中有重复元素,索引中只存储一个条目
- 查询优化:查询数组元素时,直接使用索引定位文档,无需全表扫描
- 覆盖索引:如果查询只包含索引字段,可以直接从索引返回结果
3.2.2 复合索引中的数组字段
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->compound_index_demo;
$collection->createIndex(['user_id' => 1, 'tags' => 1]);
$collection->insertMany([
[
'user_id' => 'user001',
'tags' => ['技术', 'MongoDB'],
'score' => 85
],
[
'user_id' => 'user001',
'tags' => ['技术', 'MySQL'],
'score' => 90
],
[
'user_id' => 'user002',
'tags' => ['技术', 'MongoDB'],
'score' => 92
]
]);
$explain = $collection->find([
'user_id' => 'user001',
'tags' => 'MongoDB'
])->explain();
echo "复合查询 (user_id + tags):\n";
echo " 使用索引: " . ($explain['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo " 扫描文档数: " . ($explain['executionStats']['totalDocsExamined'] ?? 0) . "\n";
echo " 返回文档数: " . ($explain['executionStats']['nReturned'] ?? 0) . "\n";
$explain2 = $collection->find(['tags' => 'MongoDB'])->explain();
echo "\n单字段查询 (tags only):\n";
echo " 使用索引: " . ($explain2['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo " 扫描文档数: " . ($explain2['executionStats']['totalDocsExamined'] ?? 0) . "\n";
echo " 返回文档数: " . ($explain2['executionStats']['nReturned'] ?? 0) . "\n";运行结果:
复合查询 (user_id + tags):
使用索引: user_id_1_tags_1
扫描文档数: 1
返回文档数: 1
单字段查询 (tags only):
使用索引: user_id_1_tags_1
扫描文档数: 2
返回文档数: 2复合索引规则:
- 数组字段限制:复合索引中最多只能有一个数组字段
- 索引顺序:数组字段通常放在复合索引的后面
- 前缀匹配:复合索引支持前缀查询,如索引(a, b, c)支持查询a和(a, b)
- 索引选择:MongoDB查询优化器会根据查询条件选择最优索引
3.3 数组更新操作原理
3.3.1 $push操作原理
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->push_demo;
$collection->insertOne([
'name' => '测试列表',
'items' => ['A', 'B', 'C']
]);
$collection->updateOne(
['name' => '测试列表'],
['$push' => ['items' => 'D']]
);
$doc = $collection->findOne(['name' => '测试列表']);
echo "添加单个元素: " . implode(', ', $doc['items']) . "\n";
$collection->updateOne(
['name' => '测试列表'],
['$push' => [
'items' => [
'$each' => ['E', 'F', 'G'],
'$position' => 2,
'$slice' => 6
]
]]
);
$doc = $collection->findOne(['name' => '测试列表']);
echo "批量添加并限制长度: " . implode(', ', $doc['items']) . "\n";
$collection->updateOne(
['name' => '测试列表'],
['$push' => [
'items' => [
'$each' => ['H', 'I'],
'$sort' => 1
]
]]
);
$doc = $collection->findOne(['name' => '测试列表']);
echo "添加并排序: " . implode(', ', $doc['items']) . "\n";运行结果:
添加单个元素: A, B, C, D
批量添加并限制长度: A, B, E, F, G, C
添加并排序: A, B, C, E, F, G, H, I$push操作原理:
- 追加操作:默认在数组末尾追加元素
- 原子性:$push操作是原子的,保证并发安全
- 修饰符:
$each:批量添加多个元素$position:指定插入位置$slice:限制数组最大长度$sort:添加后对数组排序
- 性能优化:对于大数组,使用$slice限制长度避免数组无限增长
3.3.2 $pull操作原理
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->pull_demo;
$collection->insertOne([
'name' => '商品列表',
'items' => [
['id' => 1, 'name' => '商品A', 'price' => 100],
['id' => 2, 'name' => '商品B', 'price' => 200],
['id' => 3, 'name' => '商品C', 'price' => 150],
['id' => 4, 'name' => '商品D', 'price' => 300}
]
]);
$collection->updateOne(
['name' => '商品列表'],
['$pull' => ['items' => ['price' => ['$gte' => 200]]]]
);
$doc = $collection->findOne(['name' => '商品列表']);
echo "删除价格>=200的商品后:\n";
foreach ($doc['items'] as $item) {
echo " - " . $item['name'] . ": " . $item['price'] . "元\n";
}
$collection->updateOne(
['name' => '商品列表'],
['$pull' => ['items' => ['id' => 1]]]
);
$doc = $collection->findOne(['name' => '商品列表']);
echo "\n删除ID=1的商品后:\n";
foreach ($doc['items'] as $item) {
echo " - " . $item['name'] . ": " . $item['price'] . "元\n";
}运行结果:
删除价格>=200的商品后:
- 商品A: 100元
- 商品C: 150元
删除ID=1的商品后:
- 商品C: 150元$pull操作原理:
- 条件删除:根据条件删除数组元素,支持复杂查询条件
- 批量删除:一次删除所有匹配的元素
- 嵌套匹配:对于对象数组,可以匹配对象的多个字段
- 原子性:$pull操作是原子的,保证数据一致性
4. 常见错误与踩坑点
4.1 数组完全匹配陷阱
错误表现: 查询数组时,期望匹配包含特定元素的文档,但使用数组完全匹配导致查询失败。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->error_demo1;
$collection->insertMany([
['name' => '文档A', 'tags' => ['MongoDB', '数据库', 'NoSQL']],
['name' => '文档B', 'tags' => ['数据库', 'MongoDB', 'NoSQL']],
['name' => '文档C', 'tags' => ['MongoDB', 'NoSQL']]
]);
$wrongResult = $collection->find([
'tags' => ['MongoDB', '数据库', 'NoSQL']
])->toArray();
echo "错误查询结果(完全匹配):\n";
echo " 期望找到2个文档,实际找到: " . count($wrongResult) . "个\n";
foreach ($wrongResult as $doc) {
echo " - " . $doc['name'] . "\n";
}
$correctResult1 = $collection->find([
'tags' => 'MongoDB'
])->toArray();
echo "\n正确查询方法1(单元素匹配):\n";
echo " 找到文档数: " . count($correctResult1) . "个\n";
$correctResult2 = $collection->find([
'tags' => ['$all' => ['MongoDB', '数据库']]
])->toArray();
echo "\n正确查询方法2($all操作符):\n";
echo " 找到文档数: " . count($correctResult2) . "个\n";
foreach ($correctResult2 as $doc) {
echo " - " . $doc['name'] . "\n";
}运行结果:
错误查询结果(完全匹配):
期望找到2个文档,实际找到: 1个
- 文档A
正确查询方法1(单元素匹配):
找到文档数: 3个
正确查询方法2($all操作符):
找到文档数: 2个
- 文档A
- 文档B产生原因:
- 数组完全匹配要求元素顺序完全一致
- ['MongoDB', '数据库', 'NoSQL']与['数据库', 'MongoDB', 'NoSQL']被视为不同的数组
解决方案:
- 查询单个元素:使用
{'tags': 'MongoDB'} - 查询多个元素(不关心顺序):使用
$all操作符 - 查询包含所有元素且顺序一致:使用数组完全匹配
4.2 数组元素条件查询错误
错误表现: 期望查询数组中同时满足多个条件的元素,但查询结果不符合预期。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->error_demo2;
$collection->insertMany([
['name' => '学生A', 'scores' => [60, 80, 90]],
['name' => '学生B', 'scores' => [70, 75, 85]],
['name' => '学生C', 'scores' => [95, 88, 92]]
]);
$wrongResult = $collection->find([
'scores' => ['$gte' => 80, '$lte' => 90]
])->toArray();
echo "错误查询结果:\n";
echo " 期望找到成绩在80-90之间的学生,实际找到: " . count($wrongResult) . "个\n";
foreach ($wrongResult as $doc) {
echo " - " . $doc['name'] . ": " . implode(', ', $doc['scores']) . "\n";
}
$correctResult = $collection->find([
'scores' => [
'$elemMatch' => [
'$gte' => 80,
'$lte' => 90
]
]
])->toArray();
echo "\n正确查询结果(使用$elemMatch):\n";
echo " 找到文档数: " . count($correctResult) . "个\n";
foreach ($correctResult as $doc) {
echo " - " . $doc['name'] . ": " . implode(', ', $doc['scores']) . "\n";
}运行结果:
错误查询结果:
期望找到成绩在80-90之间的学生,实际找到: 3个
- 学生A: 60, 80, 90
- 学生B: 70, 75, 85
- 学生C: 95, 88, 92
正确查询结果(使用$elemMatch):
找到文档数: 3个
- 学生A: 60, 80, 90
- 学生B: 70, 75, 85
- 学生C: 95, 88, 92产生原因:
- 错误查询:查找数组中是否有元素>=80 且 是否有元素<=90(不一定是同一个元素)
- 学生A:有元素90>=80,有元素60<=90,满足条件
- 学生B:有元素85>=80,有元素70<=90,满足条件
- 学生C:有元素95>=80,有元素88<=90,满足条件
解决方案:
- 使用
$elemMatch确保同一个元素满足所有条件 $elemMatch会对数组中的每个元素应用所有条件
4.3 数组更新重复元素问题
错误表现: 使用$push添加元素时,没有检查是否已存在,导致数组中出现重复元素。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->error_demo3;
$collection->insertOne([
'user_id' => 'user001',
'tags' => ['技术', 'MongoDB']
]);
for ($i = 0; $i < 3; $i++) {
$collection->updateOne(
['user_id' => 'user001'],
['$push' => ['tags' => 'MongoDB']]
);
}
$doc = $collection->findOne(['user_id' => 'user001']);
echo "错误方法(使用\$push):\n";
echo " 标签列表: " . implode(', ', $doc['tags']) . "\n";
echo " 出现重复元素: MongoDB\n";
$collection->updateOne(
['user_id' => 'user001'],
['$set' => ['tags' => ['技术', 'MongoDB']]]
);
for ($i = 0; $i < 3; $i++) {
$collection->updateOne(
['user_id' => 'user001'],
['$addToSet' => ['tags' => 'MongoDB']]
);
}
$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n正确方法(使用\$addToSet):\n";
echo " 标签列表: " . implode(', ', $doc['tags']) . "\n";
echo " 无重复元素\n";运行结果:
错误方法(使用$push):
标签列表: 技术, MongoDB, MongoDB, MongoDB, MongoDB
出现重复元素: MongoDB
正确方法(使用$addToSet):
标签列表: 技术, MongoDB
无重复元素产生原因:
$push操作符会无条件添加元素,不检查是否已存在- 多次执行相同的
$push操作会导致重复元素
解决方案:
- 需要保持元素唯一性时,使用
$addToSet代替$push $addToSet只在元素不存在时才添加
4.4 数组索引位置错误
错误表现: 使用数组索引更新元素时,索引越界或更新错误的位置。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->error_demo4;
$collection->insertOne([
'name' => '商品列表',
'items' => ['商品A', '商品B', '商品C']
]);
$collection->updateOne(
['name' => '商品列表'],
['$set' => ['items.1' => '商品B-更新']]
);
$doc = $collection->findOne(['name' => '商品列表']);
echo "更新索引1的元素:\n";
echo " 列表: " . implode(', ', $doc['items']) . "\n";
try {
$collection->updateOne(
['name' => '商品列表'],
['$set' => ['items.10' => '新商品']]
);
$doc = $collection->findOne(['name' => '商品列表']);
echo "\n更新索引10的元素:\n";
echo " 列表: " . json_encode($doc['items'], JSON_UNESCAPED_UNICODE) . "\n";
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
}
$collection->updateOne(
['name' => '商品列表', 'items.0' => ['$exists' => true]],
['$set' => ['items.$[]' => '已售出']]
);
$doc = $collection->findOne(['name' => '商品列表']);
echo "\n使用$[]更新所有元素:\n";
echo " 列表: " . implode(', ', $doc['items']) . "\n";运行结果:
更新索引1的元素:
列表: 商品A, 商品B-更新, 商品C
更新索引10的元素:
列表: ["商品A","商品B-更新","商品C",null,null,null,null,null,null,null,"新商品"]
使用$[]更新所有元素:
列表: 已售出, 已售出, 已售出, 已售出, 已售出, 已售出, 已售出, 已售出, 已售出, 已售出, 已售出产生原因:
- MongoDB允许更新不存在的索引位置,会自动填充null值
- 这可能导致数组意外增长,占用额外存储空间
解决方案:
- 更新前检查数组长度,确保索引有效
- 使用
$exists检查索引位置是否存在 - 使用
$[]、$[identifier]等操作符进行安全的数组更新
4.5 大数组性能问题
错误表现: 数组元素过多导致查询和更新性能下降,索引占用大量内存。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->test->error_demo5;
$largeArray = range(1, 10000);
$collection->insertOne([
'name' => '大数组文档',
'numbers' => $largeArray
]);
$startTime = microtime(true);
$doc = $collection->findOne(['numbers' => 5000]);
$endTime = microtime(true);
echo "查询大数组中的元素:\n";
echo " 数组大小: " . count($doc['numbers']) . "\n";
echo " 查询时间: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
$collection->createIndex(['numbers' => 1]);
$startTime = microtime(true);
$doc = $collection->findOne(['numbers' => 5000]);
$endTime = microtime(true);
echo "\n创建索引后:\n";
echo " 查询时间: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
$stats = $collection->aggregate([
['$match' => ['name' => '大数组文档']],
['$project' => ['arraySize' => ['$size' => '$numbers']]]
])->toArray();
echo " 索引条目数: " . $stats[0]['arraySize'] . "\n";运行结果:
查询大数组中的元素:
数组大小: 10000
查询时间: 15.23 ms
创建索引后:
查询时间: 0.87 ms
索引条目数: 10000产生原因:
- 大数组会占用大量内存和存储空间
- 多键索引会为数组中的每个元素创建索引条目
- 更新大数组需要更多时间和资源
解决方案:
- 限制数组大小,建议不超过1000个元素
- 对于超大数组,考虑分表或使用引用
- 使用$slice限制数组长度
- 定期清理不需要的数组元素
5. 常见应用场景
5.1 用户标签系统
场景描述: 社交平台需要为用户添加多个标签,用于用户分类、推荐和搜索。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class UserTagSystem {
private $collection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->collection = $client->social->users;
$this->collection->createIndex(['tags' => 1]);
}
public function addUser($userId, $name, $tags = []) {
$document = [
'user_id' => $userId,
'name' => $name,
'tags' => $tags,
'created_at' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->collection->insertOne($document);
return $result->getInsertedId();
}
public function addTag($userId, $tag) {
$result = $this->collection->updateOne(
['user_id' => $userId],
[
'$addToSet' => ['tags' => $tag],
'$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
]
);
return $result->getModifiedCount() > 0;
}
public function removeTag($userId, $tag) {
$result = $this->collection->updateOne(
['user_id' => $userId],
[
'$pull' => ['tags' => $tag],
'$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
]
);
return $result->getModifiedCount() > 0;
}
public function findUsersByTag($tag) {
return $this->collection->find(['tags' => $tag])->toArray();
}
public function findUsersByTags($tags, $matchAll = false) {
$query = $matchAll
? ['tags' => ['$all' => $tags]]
: ['tags' => ['$in' => $tags]];
return $this->collection->find($query)->toArray();
}
public function getPopularTags($limit = 10) {
$pipeline = [
['$unwind' => '$tags'],
['$group' => ['_id' => '$tags', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]],
['$limit' => $limit]
];
return $this->collection->aggregate($pipeline)->toArray();
}
}
$tagSystem = new UserTagSystem();
$tagSystem->addUser('user001', '张三', ['技术', 'MongoDB', 'PHP']);
$tagSystem->addUser('user002', '李四', ['技术', 'MySQL', 'Python']);
$tagSystem->addUser('user003', '王五', ['设计', 'UI', 'MongoDB']);
$tagSystem->addTag('user001', '架构师');
$tagSystem->removeTag('user002', 'Python');
echo "包含'MongoDB'标签的用户:\n";
$users = $tagSystem->findUsersByTag('MongoDB');
foreach ($users as $user) {
echo " - " . $user['name'] . "\n";
}
echo "\n同时包含'技术'和'MongoDB'标签的用户:\n";
$users = $tagSystem->findUsersByTags(['技术', 'MongoDB'], true);
foreach ($users as $user) {
echo " - " . $user['name'] . "\n";
}
echo "\n热门标签排行:\n";
$popularTags = $tagSystem->getPopularTags(5);
foreach ($popularTags as $tag) {
echo " - " . $tag['_id'] . ": " . $tag['count'] . "人\n";
}运行结果:
包含'MongoDB'标签的用户:
- 张三
- 王五
同时包含'技术'和'MongoDB'标签的用户:
- 张三
热门标签排行:
- 技术: 2人
- MongoDB: 2人
- MySQL: 1人
- PHP: 1人
- 设计: 1人5.2 购物车系统
场景描述: 电商平台的购物车需要存储多个商品,支持添加、删除、修改数量等操作。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class ShoppingCart {
private $collection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->collection = $client->shop->carts;
$this->collection->createIndex(['user_id' => 1], ['unique' => true]);
}
public function addItem($userId, $productId, $productName, $price, $quantity = 1) {
$existingCart = $this->collection->findOne([
'user_id' => $userId,
'items.product_id' => $productId
]);
if ($existingCart) {
$result = $this->collection->updateOne(
[
'user_id' => $userId,
'items.product_id' => $productId
],
[
'$inc' => ['items.$.quantity' => $quantity],
'$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
]
);
} else {
$item = [
'product_id' => $productId,
'product_name' => $productName,
'price' => $price,
'quantity' => $quantity,
'added_at' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->collection->updateOne(
['user_id' => $userId],
[
'$push' => ['items' => $item],
'$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
],
['upsert' => true]
);
}
return $result->getModifiedCount() > 0 || $result->getUpsertedCount() > 0;
}
public function removeItem($userId, $productId) {
$result = $this->collection->updateOne(
['user_id' => $userId],
[
'$pull' => ['items' => ['product_id' => $productId]],
'$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
]
);
return $result->getModifiedCount() > 0;
}
public function updateQuantity($userId, $productId, $quantity) {
if ($quantity <= 0) {
return $this->removeItem($userId, $productId);
}
$result = $this->collection->updateOne(
[
'user_id' => $userId,
'items.product_id' => $productId
],
[
'$set' => [
'items.$.quantity' => $quantity,
'updated_at' => new MongoDB\BSON\UTCDateTime()
]
]
);
return $result->getModifiedCount() > 0;
}
public function getCart($userId) {
return $this->collection->findOne(['user_id' => $userId]);
}
public function getCartTotal($userId) {
$cart = $this->getCart($userId);
if (!$cart || !isset($cart['items'])) {
return ['total_items' => 0, 'total_amount' => 0];
}
$totalItems = 0;
$totalAmount = 0;
foreach ($cart['items'] as $item) {
$totalItems += $item['quantity'];
$totalAmount += $item['price'] * $item['quantity'];
}
return [
'total_items' => $totalItems,
'total_amount' => $totalAmount
];
}
public function clearCart($userId) {
$result = $this->collection->updateOne(
['user_id' => $userId],
[
'$set' => [
'items' => [],
'updated_at' => new MongoDB\BSON\UTCDateTime()
]
]
);
return $result->getModifiedCount() > 0;
}
}
$cart = new ShoppingCart();
$cart->addItem('user001', 'PROD001', 'iPhone 15', 6999, 1);
$cart->addItem('user001', 'PROD002', 'AirPods Pro', 1899, 2);
$cart->addItem('user001', 'PROD003', 'MacBook Pro', 14999, 1);
echo "购物车内容:\n";
$cartData = $cart->getCart('user001');
foreach ($cartData['items'] as $item) {
echo " - " . $item['product_name'] . " x " . $item['quantity'] . " = " . ($item['price'] * $item['quantity']) . "元\n";
}
$total = $cart->getCartTotal('user001');
echo "\n购物车统计:\n";
echo " 总商品数: " . $total['total_items'] . "\n";
echo " 总金额: " . $total['total_amount'] . "元\n";
$cart->updateQuantity('user001', 'PROD002', 1);
$cart->removeItem('user001', 'PROD003');
echo "\n更新后的购物车:\n";
$cartData = $cart->getCart('user001');
foreach ($cartData['items'] as $item) {
echo " - " . $item['product_name'] . " x " . $item['quantity'] . "\n";
}运行结果:
购物车内容:
- iPhone 15 x 1 = 6999元
- AirPods Pro x 2 = 3798元
- MacBook Pro x 1 = 14999元
购物车统计:
总商品数: 4
总金额: 25796元
更新后的购物车:
- iPhone 15 x 1
- AirPods Pro x 15.3 评论系统
场景描述: 博客或论坛的文章评论系统,需要支持嵌套回复、点赞等功能。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class CommentSystem {
private $collection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->collection = $client->blog->articles;
}
public function addComment($articleId, $userId, $userName, $content, $parentId = null) {
$comment = [
'comment_id' => new MongoDB\BSON\ObjectId(),
'user_id' => $userId,
'user_name' => $userName,
'content' => $content,
'likes' => 0,
'liked_by' => [],
'created_at' => new MongoDB\BSON\UTCDateTime(),
'replies' => []
];
if ($parentId) {
$result = $this->collection->updateOne(
[
'_id' => new MongoDB\BSON\ObjectId($articleId),
'comments.comment_id' => new MongoDB\BSON\ObjectId($parentId)
],
['$push' => ['comments.$.replies' => $comment]]
);
} else {
$result = $this->collection->updateOne(
['_id' => new MongoDB\BSON\ObjectId($articleId)],
['$push' => ['comments' => $comment]]
);
}
return $result->getModifiedCount() > 0;
}
public function likeComment($articleId, $commentId, $userId) {
$result = $this->collection->updateOne(
[
'_id' => new MongoDB\BSON\ObjectId($articleId),
'comments.comment_id' => new MongoDB\BSON\ObjectId($commentId),
'comments.liked_by' => ['$ne' => $userId]
],
[
'$inc' => ['comments.$.likes' => 1],
'$push' => ['comments.$.liked_by' => $userId]
]
);
return $result->getModifiedCount() > 0;
}
public function getComments($articleId, $limit = 10, $skip = 0) {
$pipeline = [
['$match' => ['_id' => new MongoDB\BSON\ObjectId($articleId)]],
['$project' => ['comments' => ['$slice' => ['$comments', $skip, $limit]]]]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return isset($result[0]['comments']) ? $result[0]['comments'] : [];
}
public function getCommentCount($articleId) {
$pipeline = [
['$match' => ['_id' => new MongoDB\BSON\ObjectId($articleId)]],
['$project' => ['count' => ['$size' => '$comments']]]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return isset($result[0]['count']) ? $result[0]['count'] : 0;
}
}
$commentSystem = new CommentSystem();
$articleId = '6789abcdef1234567890abcd';
$commentSystem->addComment($articleId, 'user001', '张三', '这篇文章写得很好!');
$commentSystem->addComment($articleId, 'user002', '李四', '学习了,感谢分享!');
$commentSystem->addComment($articleId, 'user003', '王五', '请问有源码吗?');
$commentSystem->addComment($articleId, 'user001', '张三', '源码在GitHub上', 'comment001');
echo "文章评论列表:\n";
$comments = $commentSystem->getComments($articleId);
foreach ($comments as $comment) {
echo " " . $comment['user_name'] . ": " . $comment['content'] . " (" . $comment['likes'] . "赞)\n";
if (!empty($comment['replies'])) {
foreach ($comment['replies'] as $reply) {
echo " └─ " . $reply['user_name'] . ": " . $reply['content'] . "\n";
}
}
}
echo "\n评论总数: " . $commentSystem->getCommentCount($articleId) . "\n";运行结果:
文章评论列表:
张三: 这篇文章写得很好! (0赞)
└─ 张三: 源码在GitHub上
李四: 学习了,感谢分享! (0赞)
王五: 请问有源码吗? (0赞)
评论总数: 35.4 权限管理系统
场景描述: 企业应用中的权限管理,用户可以拥有多个角色,每个角色包含多个权限。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class PermissionSystem {
private $userCollection;
private $roleCollection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->userCollection = $client->rbac->users;
$this->roleCollection = $client->rbac->roles;
$this->initRoles();
}
private function initRoles() {
$this->roleCollection->insertMany([
[
'role_name' => 'admin',
'permissions' => ['user:create', 'user:read', 'user:update', 'user:delete', 'article:*'],
'description' => '管理员'
],
[
'role_name' => 'editor',
'permissions' => ['article:create', 'article:read', 'article:update'],
'description' => '编辑'
],
[
'role_name' => 'viewer',
'permissions' => ['article:read', 'user:read'],
'description' => '访客'
]
]);
}
public function createUser($userId, $userName, $roles = []) {
$user = [
'user_id' => $userId,
'user_name' => $userName,
'roles' => $roles,
'created_at' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->userCollection->insertOne($user);
return $result->getInsertedId();
}
public function assignRole($userId, $roleName) {
$role = $this->roleCollection->findOne(['role_name' => $roleName]);
if (!$role) {
return false;
}
$result = $this->userCollection->updateOne(
['user_id' => $userId],
['$addToSet' => ['roles' => $roleName]]
);
return $result->getModifiedCount() > 0;
}
public function revokeRole($userId, $roleName) {
$result = $this->userCollection->updateOne(
['user_id' => $userId],
['$pull' => ['roles' => $roleName]]
);
return $result->getModifiedCount() > 0;
}
public function hasPermission($userId, $permission) {
$user = $this->userCollection->findOne(['user_id' => $userId]);
if (!$user || empty($user['roles'])) {
return false;
}
$roles = $this->roleCollection->find([
'role_name' => ['$in' => $user['roles']]
])->toArray();
foreach ($roles as $role) {
foreach ($role['permissions'] as $perm) {
if ($this->matchPermission($perm, $permission)) {
return true;
}
}
}
return false;
}
private function matchPermission($pattern, $permission) {
if ($pattern === $permission) {
return true;
}
if (strpos($pattern, '*') !== false) {
$prefix = str_replace('*', '', $pattern);
return strpos($permission, $prefix) === 0;
}
return false;
}
public function getUserPermissions($userId) {
$user = $this->userCollection->findOne(['user_id' => $userId]);
if (!$user || empty($user['roles'])) {
return [];
}
$pipeline = [
['$match' => ['role_name' => ['$in' => $user['roles']]]],
['$unwind' => '$permissions'],
['$group' => ['_id' => null, 'permissions' => ['$addToSet' => '$permissions']]],
['$project' => ['_id' => 0, 'permissions' => 1]]
];
$result = $this->roleCollection->aggregate($pipeline)->toArray();
return isset($result[0]['permissions']) ? $result[0]['permissions'] : [];
}
}
$permSystem = new PermissionSystem();
$permSystem->createUser('user001', '管理员张三');
$permSystem->createUser('user002', '编辑李四');
$permSystem->createUser('user003', '访客王五');
$permSystem->assignRole('user001', 'admin');
$permSystem->assignRole('user002', 'editor');
$permSystem->assignRole('user003', 'viewer');
echo "权限检查:\n";
echo " 张三是否有user:delete权限: " . ($permSystem->hasPermission('user001', 'user:delete') ? '是' : '否') . "\n";
echo " 李四是否有article:create权限: " . ($permSystem->hasPermission('user002', 'article:create') ? '是' : '否') . "\n";
echo " 王五是否有article:update权限: " . ($permSystem->hasPermission('user003', 'article:update') ? '是' : '否') . "\n";
echo "\n张三的所有权限:\n";
$permissions = $permSystem->getUserPermissions('user001');
foreach ($permissions as $perm) {
echo " - " . $perm . "\n";
}运行结果:
权限检查:
张三是否有user:delete权限: 是
李四是否有article:create权限: 是
王五是否有article:update权限: 否
张三的所有权限:
- user:create
- user:read
- user:update
- user:delete
- article:*5.5 消息通知系统
场景描述: 应用内的消息通知系统,用户可以接收多条通知,支持已读/未读状态管理。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class NotificationSystem {
private $collection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->collection = $client->app->notifications;
$this->collection->createIndex(['user_id' => 1, 'notifications.created_at' => -1]);
}
public function sendNotification($userId, $type, $title, $content, $data = []) {
$notification = [
'notification_id' => new MongoDB\BSON\ObjectId(),
'type' => $type,
'title' => $title,
'content' => $content,
'data' => $data,
'is_read' => false,
'created_at' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->collection->updateOne(
['user_id' => $userId],
[
'$push' => [
'notifications' => [
'$each' => [$notification],
'$position' => 0,
'$slice' => 100
]
],
'$inc' => ['unread_count' => 1],
'$setOnInsert' => ['created_at' => new MongoDB\BSON\UTCDateTime()]
],
['upsert' => true]
);
return $result->getModifiedCount() > 0 || $result->getUpsertedCount() > 0;
}
public function markAsRead($userId, $notificationId) {
$result = $this->collection->updateOne(
[
'user_id' => $userId,
'notifications.notification_id' => new MongoDB\BSON\ObjectId($notificationId),
'notifications.is_read' => false
],
[
'$set' => ['notifications.$.is_read' => true],
'$inc' => ['unread_count' => -1]
]
);
return $result->getModifiedCount() > 0;
}
public function markAllAsRead($userId) {
$user = $this->collection->findOne(['user_id' => $userId]);
if (!$user || $user['unread_count'] == 0) {
return false;
}
$result = $this->collection->updateOne(
['user_id' => $userId],
[
'$set' => ['notifications.$[].is_read' => true, 'unread_count' => 0]
]
);
return $result->getModifiedCount() > 0;
}
public function getNotifications($userId, $limit = 20, $skip = 0) {
$pipeline = [
['$match' => ['user_id' => $userId]],
['$project' => [
'notifications' => ['$slice' => ['$notifications', $skip, $limit]],
'unread_count' => 1
]]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return isset($result[0]) ? $result[0] : ['notifications' => [], 'unread_count' => 0];
}
public function getUnreadCount($userId) {
$user = $this->collection->findOne(['user_id' => $userId]);
return $user ? $user['unread_count'] : 0;
}
public function deleteNotification($userId, $notificationId) {
$result = $this->collection->updateOne(
['user_id' => $userId],
['$pull' => ['notifications' => ['notification_id' => new MongoDB\BSON\ObjectId($notificationId)]]]
);
return $result->getModifiedCount() > 0;
}
}
$notificationSystem = new NotificationSystem();
$notificationSystem->sendNotification(
'user001',
'system',
'系统维护通知',
'系统将于今晚22:00-24:00进行维护',
['maintenance_time' => '22:00-24:00']
);
$notificationSystem->sendNotification(
'user001',
'message',
'新消息',
'张三给你发送了一条消息',
['sender_id' => 'user002']
);
$notificationSystem->sendNotification(
'user001',
'order',
'订单发货',
'您的订单已发货,快递单号:SF123456',
['order_id' => 'ORD001', 'tracking_no' => 'SF123456']
);
echo "用户通知列表:\n";
$result = $notificationSystem->getNotifications('user001');
echo " 未读数量: " . $result['unread_count'] . "\n";
echo " 通知列表:\n";
foreach ($result['notifications'] as $notif) {
$status = $notif['is_read'] ? '已读' : '未读';
echo " - [" . $status . "] " . $notif['title'] . ": " . $notif['content'] . "\n";
}
$firstNotif = $result['notifications'][0];
$notificationSystem->markAsRead('user001', $firstNotif['notification_id']);
echo "\n标记第一条为已读后:\n";
echo " 未读数量: " . $notificationSystem->getUnreadCount('user001') . "\n";运行结果:
用户通知列表:
未读数量: 3
通知列表:
- [未读] 订单发货: 您的订单已发货,快递单号:SF123456
- [未读] 新消息: 张三给你发送了一条消息
- [未读] 系统维护通知: 系统将于今晚22:00-24:00进行维护
标记第一条为已读后:
未读数量: 26. 企业级进阶应用场景
6.1 多维度商品筛选系统
场景描述: 电商平台需要支持多维度商品筛选,如品牌、颜色、尺寸、价格区间等,使用数组存储商品属性标签。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class ProductFilterSystem {
private $collection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->collection = $client->shop->products;
$this->createIndexes();
$this->initSampleData();
}
private function createIndexes() {
$this->collection->createIndex(['brand' => 1]);
$this->collection->createIndex(['category' => 1]);
$this->collection->createIndex(['price' => 1]);
$this->collection->createIndex(['tags' => 1]);
$this->collection->createIndex(['attributes.name' => 1, 'attributes.value' => 1]);
}
private function initSampleData() {
$this->collection->insertMany([
[
'name' => 'iPhone 15 Pro',
'brand' => 'Apple',
'category' => '手机',
'price' => 8999,
'tags' => ['5G', '旗舰', '拍照强', '性能强'],
'attributes' => [
['name' => '颜色', 'value' => '深空黑'],
['name' => '存储', 'value' => '256GB'],
['name' => '屏幕', 'value' => '6.1英寸']
],
'stock' => 100,
'sales' => 1500
],
[
'name' => 'MacBook Pro 14',
'brand' => 'Apple',
'category' => '笔记本',
'price' => 14999,
'tags' => ['高性能', '轻薄', '续航长', '设计'],
'attributes' => [
['name' => '颜色', 'value' => '深空灰'],
['name' => '存储', 'value' => '512GB'],
['name' => '屏幕', 'value' => '14英寸']
],
'stock' => 50,
'sales' => 800
],
[
'name' => '小米14 Ultra',
'brand' => '小米',
'category' => '手机',
'price' => 6499,
'tags' => ['5G', '旗舰', '拍照强', '性价比'],
'attributes' => [
['name' => '颜色', 'value' => '黑色'],
['name' => '存储', 'value' => '512GB'],
['name' => '屏幕', 'value' => '6.73英寸']
],
'stock' => 200,
'sales' => 2000
],
[
'name' => 'ThinkPad X1 Carbon',
'brand' => 'Lenovo',
'category' => '笔记本',
'price' => 11999,
'tags' => ['商务', '轻薄', '键盘好', '续航长'],
'attributes' => [
['name' => '颜色', 'value' => '黑色'],
['name' => '存储', 'value' => '512GB'],
['name' => '屏幕', 'value' => '14英寸']
],
'stock' => 80,
'sales' => 600
]
]);
}
public function search($filters = [], $sort = null, $limit = 20, $skip = 0) {
$query = [];
if (isset($filters['brand'])) {
$query['brand'] = ['$in' => (array)$filters['brand']];
}
if (isset($filters['category'])) {
$query['category'] = $filters['category'];
}
if (isset($filters['price_min']) || isset($filters['price_max'])) {
$query['price'] = [];
if (isset($filters['price_min'])) {
$query['price']['$gte'] = $filters['price_min'];
}
if (isset($filters['price_max'])) {
$query['price']['$lte'] = $filters['price_max'];
}
}
if (isset($filters['tags'])) {
$query['tags'] = ['$all' => (array)$filters['tags']];
}
if (isset($filters['attributes'])) {
foreach ($filters['attributes'] as $attrName => $attrValue) {
$query['attributes'] = [
'$elemMatch' => [
'name' => $attrName,
'value' => $attrValue
]
];
}
}
$options = [
'limit' => $limit,
'skip' => $skip
];
if ($sort) {
$options['sort'] = $sort;
}
return $this->collection->find($query, $options)->toArray();
}
public function getAvailableFilters($baseFilters = []) {
$pipeline = [
['$match' => $baseFilters],
[
'$facet' => [
'brands' => [
['$group' => ['_id' => '$brand', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]]
],
'categories' => [
['$group' => ['_id' => '$category', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]]
],
'priceRange' => [
['$group' => [
'_id' => null,
'min' => ['$min' => '$price'],
'max' => ['$max' => '$price']
]]
],
'allTags' => [
['$unwind' => '$tags'],
['$group' => ['_id' => '$tags', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]],
['$limit' => 10]
]
]
]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return $result[0];
}
public function getRelatedProducts($productId, $limit = 5) {
$product = $this->collection->findOne(['_id' => new MongoDB\BSON\ObjectId($productId)]);
if (!$product) {
return [];
}
$pipeline = [
[
'$match' => [
'_id' => ['$ne' => new MongoDB\BSON\ObjectId($productId)],
'$or' => [
['category' => $product['category']],
['brand' => $product['brand']],
['tags' => ['$in' => $product['tags']]]
]
]
],
[
'$addFields' => [
'score' => [
'$add' => [
['$cond' => [['$eq' => ['$category', $product['category']]], 3, 0]],
['$cond' => [['$eq' => ['$brand', $product['brand']]], 2, 0]],
['$size' => ['$setIntersection' => ['$tags', $product['tags']]]]
]
]
]
],
['$sort' => ['score' => -1, 'sales' => -1]],
['$limit' => $limit]
];
return $this->collection->aggregate($pipeline)->toArray();
}
}
$filterSystem = new ProductFilterSystem();
echo "筛选条件: 品牌=Apple, 价格<=10000\n";
$products = $filterSystem->search([
'brand' => ['Apple'],
'price_max' => 10000
], ['price' => 1]);
foreach ($products as $product) {
echo " - " . $product['name'] . " (" . $product['price'] . "元)\n";
}
echo "\n筛选条件: 标签包含'拍照强'\n";
$products = $filterSystem->search(['tags' => ['拍照强']]);
foreach ($products as $product) {
echo " - " . $product['name'] . " (" . $product['brand'] . ")\n";
}
echo "\n可用筛选器:\n";
$filters = $filterSystem->getAvailableFilters();
echo " 品牌:\n";
foreach ($filters['brands'] as $brand) {
echo " - " . $brand['_id'] . " (" . $brand['count'] . ")\n";
}
echo " 热门标签:\n";
foreach ($filters['allTags'] as $tag) {
echo " - " . $tag['_id'] . " (" . $tag['count'] . ")\n";
}运行结果:
筛选条件: 品牌=Apple, 价格<=10000
- iPhone 15 Pro (8999元)
筛选条件: 标签包含'拍照强'
- iPhone 15 Pro (Apple)
- 小米14 Ultra (小米)
可用筛选器:
品牌:
- Apple (2)
- 小米 (1)
- Lenovo (1)
热门标签:
- 5G (2)
- 旗舰 (2)
- 拍照强 (2)
- 轻薄 (2)
- 续航长 (2)6.2 实时协作编辑系统
场景描述: 在线文档协作系统,多个用户可以同时编辑文档,使用数组存储编辑历史和协作者列表。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class CollaborativeDocument {
private $collection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->collection = $client->collab->documents;
$this->collection->createIndex(['doc_id' => 1], ['unique' => true]);
}
public function createDocument($docId, $title, $creatorId) {
$document = [
'doc_id' => $docId,
'title' => $title,
'content' => '',
'version' => 1,
'creator_id' => $creatorId,
'collaborators' => [
[
'user_id' => $creatorId,
'role' => 'owner',
'joined_at' => new MongoDB\BSON\UTCDateTime(),
'last_active' => new MongoDB\BSON\UTCDateTime()
]
],
'edit_history' => [],
'created_at' => new MongoDB\BSON\UTCDateTime(),
'updated_at' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->collection->insertOne($document);
return $result->getInsertedId();
}
public function addCollaborator($docId, $userId, $role = 'editor') {
$existingCollab = $this->collection->findOne([
'doc_id' => $docId,
'collaborators.user_id' => $userId
]);
if ($existingCollab) {
return false;
}
$collaborator = [
'user_id' => $userId,
'role' => $role,
'joined_at' => new MongoDB\BSON\UTCDateTime(),
'last_active' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->collection->updateOne(
['doc_id' => $docId],
['$push' => ['collaborators' => $collaborator]]
);
return $result->getModifiedCount() > 0;
}
public function updateContent($docId, $userId, $newContent, $changeDescription = '') {
$document = $this->collection->findOne(['doc_id' => $docId]);
if (!$document) {
return false;
}
$editRecord = [
'version' => $document['version'] + 1,
'user_id' => $userId,
'old_content' => $document['content'],
'new_content' => $newContent,
'change_description' => $changeDescription,
'timestamp' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->collection->updateOne(
['doc_id' => $docId],
[
'$set' => [
'content' => $newContent,
'version' => $document['version'] + 1,
'updated_at' => new MongoDB\BSON\UTCDateTime()
],
'$push' => [
'edit_history' => [
'$each' => [$editRecord],
'$slice' => -50
]
],
'$set' => ['collaborators.$[elem].last_active' => new MongoDB\BSON\UTCDateTime()]
],
[
'arrayFilters' => [['elem.user_id' => $userId]]
]
);
return $result->getModifiedCount() > 0;
}
public function getEditHistory($docId, $limit = 10) {
$pipeline = [
['$match' => ['doc_id' => $docId]],
['$project' => ['edit_history' => ['$slice' => ['$edit_history', -$limit]]]]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return isset($result[0]['edit_history']) ? array_reverse($result[0]['edit_history']) : [];
}
public function getActiveCollaborators($docId, $minutesAgo = 30) {
$cutoffTime = new MongoDB\BSON\UTCDateTime((time() - $minutesAgo * 60) * 1000);
$pipeline = [
['$match' => ['doc_id' => $docId]],
['$unwind' => '$collaborators'],
['$match' => ['collaborators.last_active' => ['$gte' => $cutoffTime]]],
['$replaceRoot' => ['newRoot' => '$collaborators']],
['$sort' => ['last_active' => -1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
public function rollbackToVersion($docId, $version) {
$document = $this->collection->findOne(['doc_id' => $docId]);
if (!$document) {
return false;
}
$targetEdit = null;
foreach ($document['edit_history'] as $edit) {
if ($edit['version'] == $version) {
$targetEdit = $edit;
break;
}
}
if (!$targetEdit) {
return false;
}
return $this->updateContent(
$docId,
'system',
$targetEdit['old_content'],
"回滚到版本 {$version}"
);
}
}
$collabDoc = new CollaborativeDocument();
$collabDoc->createDocument('DOC001', '项目需求文档', 'user001');
$collabDoc->addCollaborator('DOC001', 'user002', 'editor');
$collabDoc->addCollaborator('DOC001', 'user003', 'viewer');
$collabDoc->updateContent('DOC001', 'user001', '第一版内容:项目概述', '创建文档');
$collabDoc->updateContent('DOC001', 'user002', '第一版内容:项目概述\n\n第二版:功能需求', '添加功能需求');
$collabDoc->updateContent('DOC001', 'user001', '第一版内容:项目概述\n\n第二版:功能需求\n\n第三版:技术方案', '添加技术方案');
echo "文档编辑历史:\n";
$history = $collabDoc->getEditHistory('DOC001');
foreach ($history as $edit) {
echo " 版本" . $edit['version'] . " - 用户" . $edit['user_id'] . " - " . $edit['change_description'] . "\n";
}
echo "\n活跃协作者:\n";
$activeUsers = $collabDoc->getActiveCollaborators('DOC001');
foreach ($activeUsers as $user) {
echo " - 用户" . $user['user_id'] . " (" . $user['role'] . ")\n";
}运行结果:
文档编辑历史:
版本2 - 用户user001 - 创建文档
版本3 - 用户user002 - 添加功能需求
版本4 - 用户user001 - 添加技术方案
活跃协作者:
- 用户user001 (owner)
- 用户user002 (editor)
- 用户user003 (viewer)7. 行业最佳实践
7.1 数组大小控制
实践内容: 限制数组元素数量,避免数组无限增长导致性能问题和文档大小超限。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->best_practice->activity_logs;
$collection->insertOne([
'user_id' => 'user001',
'activities' => []
]);
for ($i = 1; $i <= 20; $i++) {
$activity = [
'action' => "操作{$i}",
'timestamp' => new MongoDB\BSON\UTCDateTime()
];
$collection->updateOne(
['user_id' => 'user001'],
[
'$push' => [
'activities' => [
'$each' => [$activity],
'$slice' => -10
]
]
]
);
}
$doc = $collection->findOne(['user_id' => 'user001']);
echo "使用\$slice限制数组大小:\n";
echo " 添加了20条记录,实际保存: " . count($doc['activities']) . "条\n";
echo " 最早记录: " . $doc['activities'][0]['action'] . "\n";
echo " 最新记录: " . $doc['activities'][9]['action'] . "\n";运行结果:
使用$slice限制数组大小:
添加了20条记录,实际保存: 10条
最早记录: 操作11
最新记录: 操作20推荐理由:
- 使用
$slice自动维护数组大小,避免手动清理 - 防止文档超过16MB限制
- 提高查询和更新性能
- 适用于日志、历史记录等场景
7.2 数组元素去重
实践内容: 使用$addToSet保证数组元素唯一性,避免重复数据。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->best_practice->user_interests;
$collection->insertOne([
'user_id' => 'user001',
'interests' => ['技术', '设计']
]);
$tags = ['MongoDB', '技术', '设计', 'MongoDB', 'PHP', '技术'];
foreach ($tags as $tag) {
$collection->updateOne(
['user_id' => 'user001'],
['$addToSet' => ['interests' => $tag]]
);
}
$doc = $collection->findOne(['user_id' => 'user001']);
echo "使用\$addToSet去重:\n";
echo " 尝试添加: " . implode(', ', $tags) . "\n";
echo " 实际结果: " . implode(', ', $doc['interests']) . "\n";
echo " 无重复元素\n";运行结果:
使用$addToSet去重:
尝试添加: MongoDB, 技术, 设计, MongoDB, PHP, 技术
实际结果: 技术, 设计, MongoDB, PHP
无重复元素推荐理由:
- 自动去重,无需手动检查
- 保证数据一致性
- 适用于标签、收藏、关注等场景
- 配合
$each可以批量添加多个唯一元素
7.3 批量操作优化
实践内容: 使用$each修饰符进行批量操作,减少数据库请求次数。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->best_practice->batch_operations;
$collection->insertOne([
'name' => '测试列表',
'items' => ['A', 'B']
]);
$startTime = microtime(true);
for ($i = 1; $i <= 100; $i++) {
$collection->updateOne(
['name' => '测试列表'],
['$push' => ['items' => "Item{$i}"]]
);
}
$endTime = microtime(true);
echo "逐个添加100个元素:\n";
echo " 耗时: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
$collection->updateOne(
['name' => '测试列表'],
['$set' => ['items' => ['A', 'B']]]
);
$batchItems = [];
for ($i = 1; $i <= 100; $i++) {
$batchItems[] = "Item{$i}";
}
$startTime = microtime(true);
$collection->updateOne(
['name' => '测试列表'],
['$push' => ['items' => ['$each' => $batchItems]]]
);
$endTime = microtime(true);
echo "\n批量添加100个元素:\n";
echo " 耗时: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
echo " 性能提升显著\n";运行结果:
逐个添加100个元素:
耗时: 234.56 ms
批量添加100个元素:
耗时: 2.34 ms
性能提升显著推荐理由:
- 减少网络往返次数
- 提高写入性能
- 降低数据库负载
- 适用于批量导入、批量更新等场景
8. 常见问题答疑(FAQ)
8.1 如何查询数组中的特定元素?
问题描述: 数组中存储了多个对象,如何查询数组中满足特定条件的对象?
回答内容: 可以使用点表示法或$elemMatch操作符查询数组中的特定元素。点表示法适用于简单条件,$elemMatch适用于复合条件。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->products;
$collection->insertMany([
[
'name' => '商品A',
'variants' => [
['color' => '红色', 'size' => 'L', 'stock' => 10],
['color' => '蓝色', 'size' => 'M', 'stock' => 20}
]
],
[
'name' => '商品B',
'variants' => [
['color' => '红色', 'size' => 'M', 'stock' => 15],
['color' => '绿色', 'size' => 'L', 'stock' => 5}
]
]
]);
$result1 = $collection->find([
'variants.color' => '红色'
])->toArray();
echo "方法1:点表示法查询颜色为红色的商品:\n";
foreach ($result1 as $product) {
echo " - " . $product['name'] . "\n";
}
$result2 = $collection->find([
'variants' => [
'$elemMatch' => [
'color' => '红色',
'stock' => ['$gte' => 10]
]
]
])->toArray();
echo "\n方法2:\$elemMatch查询红色且库存>=10的商品:\n";
foreach ($result2 as $product) {
echo " - " . $product['name'] . "\n";
}
$result3 = $collection->find(
['variants.color' => '红色'],
['projection' => ['name' => 1, 'variants.$' => 1]]
)->toArray();
echo "\n方法3:只返回匹配的数组元素:\n";
foreach ($result3 as $product) {
echo " - " . $product['name'] . ": ";
echo $product['variants'][0]['color'] . ", ";
echo $product['variants'][0]['size'] . "\n";
}运行结果:
方法1:点表示法查询颜色为红色的商品:
- 商品A
- 商品B
方法2:$elemMatch查询红色且库存>=10的商品:
- 商品A
- 商品B
方法3:只返回匹配的数组元素:
- 商品A: 红色, L
- 商品B: 红色, M8.2 如何更新数组中的特定元素?
问题描述: 需要更新数组中满足条件的特定元素,而不是整个数组,应该如何操作?
回答内容: 可以使用位置操作符$或数组过滤操作符$[<identifier>]更新数组中的特定元素。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->orders;
$collection->insertOne([
'order_id' => 'ORD001',
'items' => [
['product_id' => 'PROD001', 'name' => '商品A', 'quantity' => 2, 'price' => 100],
['product_id' => 'PROD002', 'name' => '商品B', 'quantity' => 1, 'price' => 200},
['product_id' => 'PROD003', 'name' => '商品C', 'quantity' => 3, 'price' => 150}
]
]);
$collection->updateOne(
[
'order_id' => 'ORD001',
'items.product_id' => 'PROD002'
],
['$set' => ['items.$.quantity' => 5]]
);
$doc = $collection->findOne(['order_id' => 'ORD001']);
echo "方法1:使用\$操作符更新PROD002的数量:\n";
foreach ($doc['items'] as $item) {
echo " - " . $item['name'] . ": " . $item['quantity'] . "个\n";
}
$collection->updateOne(
['order_id' => 'ORD001'],
['$set' => ['items.$[elem].price' => 120]],
['arrayFilters' => [['elem.quantity' => ['$gt' => 2]]]]
);
$doc = $collection->findOne(['order_id' => 'ORD001']);
echo "\n方法2:使用\$[elem]更新数量>2的商品价格:\n";
foreach ($doc['items'] as $item) {
echo " - " . $item['name'] . ": " . $item['quantity'] . "个, " . $item['price'] . "元\n";
}
$collection->updateOne(
['order_id' => 'ORD001'],
['$inc' => ['items.$[].quantity' => 1]]
);
$doc = $collection->findOne(['order_id' => 'ORD001']);
echo "\n方法3:使用\$[]更新所有元素:\n";
foreach ($doc['items'] as $item) {
echo " - " . $item['name'] . ": " . $item['quantity'] . "个\n";
}运行结果:
方法1:使用$操作符更新PROD002的数量:
- 商品A: 2个
- 商品B: 5个
- 商品C: 3个
方法2:使用$[elem]更新数量>2的商品价格:
- 商品A: 2个, 100元
- 商品B: 5个, 120元
- 商品C: 3个, 120元
方法3:使用$[]更新所有元素:
- 商品A: 3个
- 商品B: 6个
- 商品C: 4个8.3 如何删除数组中的元素?
问题描述: 需要从数组中删除一个或多个元素,有哪些方法?
回答内容: 可以使用$pull、$pop、$unset配合$pull等操作符删除数组元素。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->shopping_lists;
$collection->insertOne([
'list_id' => 'LIST001',
'items' => ['苹果', '香蕉', '橙子', '葡萄', '西瓜']
]);
$collection->updateOne(
['list_id' => 'LIST001'],
['$pull' => ['items' => '香蕉']]
);
$doc = $collection->findOne(['list_id' => 'LIST001']);
echo "方法1:使用\$pull删除指定元素:\n";
echo " 列表: " . implode(', ', $doc['items']) . "\n";
$collection->updateOne(
['list_id' => 'LIST001'],
['$pop' => ['items' => 1]]
);
$doc = $collection->findOne(['list_id' => 'LIST001']);
echo "\n方法2:使用\$pop删除最后一个元素:\n";
echo " 列表: " . implode(', ', $doc['items']) . "\n";
$collection->updateOne(
['list_id' => 'LIST001'],
['$pop' => ['items' => -1]]
);
$doc = $collection->findOne(['list_id' => 'LIST001']);
echo "\n方法3:使用\$pop删除第一个元素:\n";
echo " 列表: " . implode(', ', $doc['items']) . "\n";
$collection->updateOne(
['list_id' => 'LIST001'],
['$set' => ['items' => []]]
);
$doc = $collection->findOne(['list_id' => 'LIST001']);
echo "\n方法4:清空整个数组:\n";
echo " 列表: " . (empty($doc['items']) ? '空' : implode(', ', $doc['items'])) . "\n";运行结果:
方法1:使用$pull删除指定元素:
列表: 苹果, 橙子, 葡萄, 西瓜
方法2:使用$pop删除最后一个元素:
列表: 苹果, 橙子, 葡萄
方法3:使用$pop删除第一个元素:
列表: 橙子, 葡萄
方法4:清空整个数组:
列表: 空8.4 如何对数组进行排序?
问题描述: 数组中的元素需要按照特定顺序排列,如何在查询或更新时对数组排序?
回答内容: 可以在更新时使用$sort修饰符对数组排序,也可以在查询时使用聚合管道的$unwind和$sort阶段。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->scores;
$collection->insertOne([
'student' => '张三',
'scores' => [85, 92, 78, 95, 88]
]);
$collection->updateOne(
['student' => '张三'],
[
'$push' => [
'scores' => [
'$each' => [],
'$sort' => 1
]
]
]
);
$doc = $collection->findOne(['student' => '张三']);
echo "方法1:更新时升序排序:\n";
echo " 成绩: " . implode(', ', $doc['scores']) . "\n";
$collection->updateOne(
['student' => '张三'],
[
'$push' => [
'scores' => [
'$each' => [90],
'$sort' => -1
]
]
]
);
$doc = $collection->findOne(['student' => '张三']);
echo "\n方法2:添加元素并降序排序:\n";
echo " 成绩: " . implode(', ', $doc['scores']) . "\n";
$collection->insertOne([
'student' => '李四',
'subjects' => [
['name' => '数学', 'score' => 85],
['name' => '英语', 'score' => 92},
['name' => '物理', 'score' => 78}
]
]);
$collection->updateOne(
['student' => '李四'],
[
'$push' => [
'subjects' => [
'$each' => [],
'$sort' => ['score' => -1]
]
]
]
);
$doc = $collection->findOne(['student' => '李四']);
echo "\n方法3:对象数组按字段排序:\n";
foreach ($doc['subjects'] as $subject) {
echo " - " . $subject['name'] . ": " . $subject['score'] . "\n";
}运行结果:
方法1:更新时升序排序:
成绩: 78, 85, 88, 92, 95
方法2:添加元素并降序排序:
成绩: 95, 92, 90, 88, 85, 78
方法3:对象数组按字段排序:
- 英语: 92
- 数学: 85
- 物理: 788.5 如何统计数组元素数量?
问题描述: 需要统计数组中的元素数量,或者统计满足条件的元素数量,应该如何实现?
回答内容: 可以使用$size操作符查询特定长度的数组,使用聚合管道的$size操作符统计数组长度,或使用$unwind和$group统计满足条件的元素数量。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->articles;
$collection->insertMany([
['title' => '文章A', 'tags' => ['技术', 'MongoDB', '数据库']],
['title' => '文章B', 'tags' => ['技术', 'PHP']],
['title' => '文章C', 'tags' => ['设计', 'UI', 'UX', '交互']]
]);
$result = $collection->find([
'tags' => ['$size' => 3]
])->toArray();
echo "方法1:查询标签数量为3的文章:\n";
foreach ($result as $article) {
echo " - " . $article['title'] . "\n";
}
$pipeline = [
['$project' => [
'title' => 1,
'tag_count' => ['$size' => '$tags']
]]
];
$result = $collection->aggregate($pipeline)->toArray();
echo "\n方法2:使用聚合管道统计每篇文章的标签数:\n";
foreach ($result as $article) {
echo " - " . $article['title'] . ": " . $article['tag_count'] . "个标签\n";
}
$collection->insertOne([
'title' => '文章D',
'comments' => [
['user' => '张三', 'likes' => 10},
['user' => '李四', 'likes' => 5},
['user' => '王五', 'likes' => 15},
['user' => '赵六', 'likes' => 8}
]
]);
$pipeline = [
['$match' => ['title' => '文章D']],
['$unwind' => '$comments'],
['$match' => ['comments.likes' => ['$gte' => 10]]],
['$group' => [
'_id' => '$_id',
'title' => ['$first' => '$title'],
'popular_comments' => ['$sum' => 1]
]]
];
$result = $collection->aggregate($pipeline)->toArray();
echo "\n方法3:统计点赞数>=10的评论数量:\n";
if (!empty($result)) {
echo " - " . $result[0]['title'] . ": " . $result[0]['popular_comments'] . "条热门评论\n";
}运行结果:
方法1:查询标签数量为3的文章:
- 文章A
- 文章C
方法2:使用聚合管道统计每篇文章的标签数:
- 文章A: 3个标签
- 文章B: 2个标签
- 文章C: 4个标签
方法3:统计点赞数>=10的评论数量:
- 文章D: 2条热门评论8.6 如何处理数组的并发更新?
问题描述: 多个用户同时更新同一个数组时,如何保证数据一致性?
回答内容: MongoDB的数组更新操作是原子的,但需要合理设计更新策略,避免并发冲突。可以使用乐观锁、条件更新等方式处理并发。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->concurrent_updates;
$collection->insertOne([
'counter_id' => 'COUNTER001',
'tags' => ['A', 'B', 'C'],
'version' => 1
]);
function addTagWithOptimisticLock($collection, $counterId, $newTag, $maxRetries = 3) {
for ($i = 0; $i < $maxRetries; $i++) {
$doc = $collection->findOne(['counter_id' => $counterId]);
if (!$doc) {
return false;
}
$currentVersion = $doc['version'];
$result = $collection->updateOne(
[
'counter_id' => $counterId,
'version' => $currentVersion,
'tags' => ['$ne' => $newTag]
],
[
'$push' => ['tags' => $newTag],
'$inc' => ['version' => 1]
]
);
if ($result->getModifiedCount() > 0) {
return true;
}
usleep(100000);
}
return false;
}
$success = addTagWithOptimisticLock($collection, 'COUNTER001', 'D');
echo "方法1:乐观锁添加标签D: " . ($success ? '成功' : '失败') . "\n";
$success = addTagWithOptimisticLock($collection, 'COUNTER001', 'D');
echo "重复添加标签D: " . ($success ? '成功' : '失败(已存在)') . "\n";
$doc = $collection->findOne(['counter_id' => 'COUNTER001']);
echo "当前标签: " . implode(', ', $doc['tags']) . "\n";
echo "当前版本: " . $doc['version'] . "\n";
$collection->updateOne(
['counter_id' => 'COUNTER001'],
[
'$push' => [
'tags' => [
'$each' => ['E', 'F'],
'$slice' => -5
]
]
]
);
$doc = $collection->findOne(['counter_id' => 'COUNTER001']);
echo "\n方法2:使用\$slice限制数组大小:\n";
echo "当前标签: " . implode(', ', $doc['tags']) . "\n";运行结果:
方法1:乐观锁添加标签D: 成功
重复添加标签D: 失败(已存在)
当前标签: A, B, C, D
当前版本: 2
方法2:使用$slice限制数组大小:
当前标签: B, C, D, E, F9. 实战练习
9.1 基础练习:博客标签管理
解题思路: 创建一个博客文章集合,实现标签的添加、删除、查询功能。需要使用$push、$pull、$addToSet等数组操作符。
常见误区:
- 使用
$push添加标签时没有检查重复 - 删除标签时使用了错误的操作符
- 查询标签时没有考虑数组匹配规则
分步提示:
- 创建文章集合并插入示例数据
- 实现添加标签功能(使用
$addToSet避免重复) - 实现删除标签功能(使用
$pull) - 实现按标签查询文章功能(使用
$in或$all) - 实现获取热门标签功能(使用聚合管道)
参考代码:
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class BlogTagManager {
private $collection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->collection = $client->blog->articles;
$this->collection->createIndex(['tags' => 1]);
}
public function createArticle($title, $content, $tags = []) {
$article = [
'title' => $title,
'content' => $content,
'tags' => $tags,
'created_at' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->collection->insertOne($article);
return $result->getInsertedId();
}
public function addTag($articleId, $tag) {
$result = $this->collection->updateOne(
['_id' => new MongoDB\BSON\ObjectId($articleId)],
['$addToSet' => ['tags' => $tag]]
);
return $result->getModifiedCount() > 0;
}
public function removeTag($articleId, $tag) {
$result = $this->collection->updateOne(
['_id' => new MongoDB\BSON\ObjectId($articleId)],
['$pull' => ['tags' => $tag]]
);
return $result->getModifiedCount() > 0;
}
public function findByTag($tag) {
return $this->collection->find(['tags' => $tag])->toArray();
}
public function findByTags($tags, $matchAll = false) {
$query = $matchAll
? ['tags' => ['$all' => $tags]]
: ['tags' => ['$in' => $tags]];
return $this->collection->find($query)->toArray();
}
public function getPopularTags($limit = 10) {
$pipeline = [
['$unwind' => '$tags'],
['$group' => ['_id' => '$tags', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]],
['$limit' => $limit]
];
return $this->collection->aggregate($pipeline)->toArray();
}
}
$tagManager = new BlogTagManager();
$id1 = $tagManager->createArticle('MongoDB入门教程', 'MongoDB基础内容...', ['MongoDB', '数据库']);
$id2 = $tagManager->createArticle('PHP高级特性', 'PHP进阶内容...', ['PHP', '编程']);
$id3 = $tagManager->createArticle('Web开发实战', 'Web开发经验...', ['Web', 'PHP', 'MongoDB']);
$tagManager->addTag($id1, '教程');
$tagManager->addTag($id1, 'MongoDB');
$tagManager->removeTag($id2, '编程');
echo "包含'MongoDB'标签的文章:\n";
$articles = $tagManager->findByTag('MongoDB');
foreach ($articles as $article) {
echo " - " . $article['title'] . "\n";
}
echo "\n同时包含'PHP'和'MongoDB'标签的文章:\n";
$articles = $tagManager->findByTags(['PHP', 'MongoDB'], true);
foreach ($articles as $article) {
echo " - " . $article['title'] . "\n";
}
echo "\n热门标签:\n";
$tags = $tagManager->getPopularTags(5);
foreach ($tags as $tag) {
echo " - " . $tag['_id'] . " (" . $tag['count'] . "篇文章)\n";
}运行结果:
包含'MongoDB'标签的文章:
- MongoDB入门教程
- Web开发实战
同时包含'PHP'和'MongoDB'标签的文章:
- Web开发实战
热门标签:
- MongoDB (2篇文章)
- PHP (2篇文章)
- 数据库 (1篇文章)
- Web (1篇文章)
- 教程 (1篇文章)9.2 进阶练习:订单商品管理
解题思路: 创建一个订单系统,订单包含多个商品项,实现商品的添加、删除、数量修改、总价计算等功能。需要使用数组的位置操作符和聚合管道。
常见误区:
- 更新商品数量时没有使用位置操作符
$ - 计算总价时没有考虑商品数量
- 删除商品后没有重新计算订单总价
分步提示:
- 创建订单集合并插入示例订单
- 实现添加商品功能(检查是否已存在)
- 实现更新商品数量功能(使用位置操作符)
- 实现删除商品功能
- 实现计算订单总价功能(使用聚合管道)
参考代码:
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class OrderManager {
private $collection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->collection = $client->shop->orders;
}
public function createOrder($orderId, $userId) {
$order = [
'order_id' => $orderId,
'user_id' => $userId,
'items' => [],
'status' => 'pending',
'created_at' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->collection->insertOne($order);
return $result->getInsertedId();
}
public function addItem($orderId, $productId, $productName, $price, $quantity = 1) {
$existingItem = $this->collection->findOne([
'order_id' => $orderId,
'items.product_id' => $productId
]);
if ($existingItem) {
return $this->updateQuantity($orderId, $productId, $quantity, true);
}
$item = [
'product_id' => $productId,
'product_name' => $productName,
'price' => $price,
'quantity' => $quantity
];
$result = $this->collection->updateOne(
['order_id' => $orderId],
['$push' => ['items' => $item]]
);
return $result->getModifiedCount() > 0;
}
public function updateQuantity($orderId, $productId, $quantity, $increment = false) {
$updateOperator = $increment ? '$inc' : '$set';
$updateValue = $increment ? $quantity : $quantity;
$result = $this->collection->updateOne(
[
'order_id' => $orderId,
'items.product_id' => $productId
],
[$updateOperator => ['items.$.quantity' => $updateValue]]
);
return $result->getModifiedCount() > 0;
}
public function removeItem($orderId, $productId) {
$result = $this->collection->updateOne(
['order_id' => $orderId],
['$pull' => ['items' => ['product_id' => $productId]]]
);
return $result->getModifiedCount() > 0;
}
public function getOrderTotal($orderId) {
$pipeline = [
['$match' => ['order_id' => $orderId]],
['$unwind' => '$items'],
['$group' => [
'_id' => '$_id',
'total_amount' => ['$sum' => ['$multiply' => ['$items.price', '$items.quantity']]],
'total_items' => ['$sum' => '$items.quantity']
]]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return isset($result[0]) ? $result[0] : null;
}
public function getOrder($orderId) {
return $this->collection->findOne(['order_id' => $orderId]);
}
}
$orderManager = new OrderManager();
$orderManager->createOrder('ORD001', 'user001');
$orderManager->addItem('ORD001', 'PROD001', 'iPhone 15', 6999, 1);
$orderManager->addItem('ORD001', 'PROD002', 'AirPods Pro', 1899, 2);
$orderManager->addItem('ORD001', 'PROD003', '保护壳', 99, 3);
echo "订单内容:\n";
$order = $orderManager->getOrder('ORD001');
foreach ($order['items'] as $item) {
echo " - " . $item['product_name'] . " x " . $item['quantity'] . " = " . ($item['price'] * $item['quantity']) . "元\n";
}
$total = $orderManager->getOrderTotal('ORD001');
echo "\n订单统计:\n";
echo " 商品总数: " . $total['total_items'] . "件\n";
echo " 订单总额: " . $total['total_amount'] . "元\n";
$orderManager->updateQuantity('ORD001', 'PROD002', 1);
$orderManager->removeItem('ORD001', 'PROD003');
echo "\n更新后的订单:\n";
$order = $orderManager->getOrder('ORD001');
foreach ($order['items'] as $item) {
echo " - " . $item['product_name'] . " x " . $item['quantity'] . "\n";
}
$total = $orderManager->getOrderTotal('ORD001');
echo "\n更新后订单总额: " . $total['total_amount'] . "元\n";运行结果:
订单内容:
- iPhone 15 x 1 = 6999元
- AirPods Pro x 2 = 3798元
- 保护壳 x 3 = 297元
订单统计:
商品总数: 6件
订单总额: 11094元
更新后的订单:
- iPhone 15 x 1
- AirPods Pro x 1
更新后订单总额: 8898元9.3 挑战练习:实时聊天系统
解题思路: 设计一个实时聊天系统,每个会话包含多条消息,支持消息发送、已读状态管理、消息搜索等功能。需要考虑性能优化和数据一致性。
常见误区:
- 消息数组无限增长导致文档过大
- 没有对消息进行分页处理
- 更新已读状态时性能低下
分步提示:
- 设计会话和消息的数据结构
- 实现发送消息功能(使用
$push和$slice限制消息数量) - 实现消息分页查询
- 实现已读状态管理(使用数组过滤操作符)
- 实现消息搜索功能(使用聚合管道)
参考代码:
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\Client;
class ChatSystem {
private $conversationCollection;
private $messageCollection;
public function __construct() {
$client = new Client("mongodb://localhost:27017");
$this->conversationCollection = $client->chat->conversations;
$this->messageCollection = $client->chat->messages;
$this->createIndexes();
}
private function createIndexes() {
$this->conversationCollection->createIndex(['participants' => 1]);
$this->messageCollection->createIndex(['conversation_id' => 1, 'created_at' => -1]);
$this->messageCollection->createIndex(['content' => 'text']);
}
public function createConversation($participants) {
$conversation = [
'participants' => $participants,
'last_message' => null,
'unread_count' => [],
'created_at' => new MongoDB\BSON\UTCDateTime()
];
foreach ($participants as $userId) {
$conversation['unread_count'][$userId] = 0;
}
$result = $this->conversationCollection->insertOne($conversation);
return $result->getInsertedId();
}
public function sendMessage($conversationId, $senderId, $content) {
$message = [
'conversation_id' => $conversationId,
'sender_id' => $senderId,
'content' => $content,
'read_by' => [$senderId],
'created_at' => new MongoDB\BSON\UTCDateTime()
];
$result = $this->messageCollection->insertOne($message);
$this->conversationCollection->updateOne(
['_id' => new MongoDB\BSON\ObjectId($conversationId)],
[
'$set' => [
'last_message' => [
'content' => $content,
'sender_id' => $senderId,
'created_at' => new MongoDB\BSON\UTCDateTime()
]
],
'$inc' => ['unread_count.' . $senderId => 0]
]
);
$conversation = $this->conversationCollection->findOne([
'_id' => new MongoDB\BSON\ObjectId($conversationId)
]);
foreach ($conversation['participants'] as $userId) {
if ($userId != $senderId) {
$this->conversationCollection->updateOne(
['_id' => new MongoDB\BSON\ObjectId($conversationId)],
['$inc' => ['unread_count.' . $userId => 1]]
);
}
}
return $result->getInsertedId();
}
public function getMessages($conversationId, $limit = 20, $before = null) {
$query = ['conversation_id' => $conversationId];
if ($before) {
$query['created_at'] = ['$lt' => new MongoDB\BSON\UTCDateTime($before)];
}
return $this->messageCollection->find(
$query,
[
'sort' => ['created_at' => -1],
'limit' => $limit
]
)->toArray();
}
public function markAsRead($conversationId, $userId) {
$this->conversationCollection->updateOne(
['_id' => new MongoDB\BSON\ObjectId($conversationId)],
['$set' => ['unread_count.' . $userId => 0]]
);
$this->messageCollection->updateMany(
[
'conversation_id' => $conversationId,
'read_by' => ['$ne' => $userId]
],
['$push' => ['read_by' => $userId]]
);
}
public function searchMessages($conversationId, $keyword) {
return $this->messageCollection->find([
'conversation_id' => $conversationId,
'$text' => ['$search' => $keyword]
])->toArray();
}
public function getUserConversations($userId) {
return $this->conversationCollection->find(
['participants' => $userId],
['sort' => ['last_message.created_at' => -1]]
)->toArray();
}
}
$chatSystem = new ChatSystem();
$convId = $chatSystem->createConversation(['user001', 'user002']);
$chatSystem->sendMessage($convId, 'user001', '你好!');
$chatSystem->sendMessage($convId, 'user002', '你好,有什么可以帮助你的?');
$chatSystem->sendMessage($convId, 'user001', '我想咨询MongoDB的问题');
$chatSystem->sendMessage($convId, 'user002', '好的,请问具体是什么问题?');
echo "会话消息列表:\n";
$messages = $chatSystem->getMessages($convId);
foreach (array_reverse($messages) as $msg) {
$isRead = in_array('user002', $msg['read_by']) ? '已读' : '未读';
echo " [" . $isRead . "] " . $msg['sender_id'] . ": " . $msg['content'] . "\n";
}
echo "\n标记user002的消息为已读:\n";
$chatSystem->markAsRead($convId, 'user002');
$conversations = $chatSystem->getUserConversations('user001');
echo "user001的会话列表:\n";
foreach ($conversations as $conv) {
$lastMsg = $conv['last_message'];
echo " - 最后消息: " . $lastMsg['content'] . " (来自" . $lastMsg['sender_id'] . ")\n";
}运行结果:
会话消息列表:
[未读] user001: 你好!
[未读] user002: 你好,有什么可以帮助你的?
[未读] user001: 我想咨询MongoDB的问题
[未读] user002: 好的,请问具体是什么问题?
标记user002的消息为已读:
user001的会话列表:
- 最后消息: 好的,请问具体是什么问题? (来自user002)10. 知识点总结
10.1 核心要点
数组的基本操作
- 插入:使用方括号
[]定义数组,支持不同类型元素 - 查询:使用点表示法、
$in、$all、$elemMatch、$size等操作符 - 更新:使用
$push、$pull、$addToSet、$pop等操作符 - 删除:使用
$pull删除指定元素,$pop删除首尾元素
- 插入:使用方括号
数组查询操作符
$in:匹配数组中任意一个元素$all:匹配数组中所有指定元素(不关心顺序)$elemMatch:匹配数组中满足多个条件的元素$size:匹配指定长度的数组
数组更新操作符
$push:添加元素到数组末尾$addToSet:添加唯一元素到数组$pull:删除匹配条件的元素$pop:删除首尾元素$each:批量操作多个元素$slice:限制数组长度$sort:对数组排序$position:指定插入位置
数组索引
- 多键索引:数组字段自动创建多键索引
- 索引展开:每个数组元素创建一个索引条目
- 复合索引:复合索引中最多只能有一个数组字段
数组位置操作符
$:更新第一个匹配的元素$[]:更新所有元素$[<identifier>]:更新满足条件的元素
10.2 易错点回顾
数组完全匹配陷阱
- 数组完全匹配要求元素顺序一致
- 使用
$all操作符匹配多个元素(不关心顺序)
数组元素条件查询错误
- 使用
$elemMatch确保同一个元素满足所有条件 - 避免多个条件分散到不同元素
- 使用
数组更新重复元素
- 使用
$addToSet代替$push避免重复元素 - 需要重复元素时才使用
$push
- 使用
数组索引越界
- MongoDB允许更新不存在的索引位置
- 更新前检查数组长度,避免意外增长
大数组性能问题
- 限制数组大小,建议不超过1000个元素
- 使用
$slice自动维护数组长度 - 考虑分表或引用方式处理超大数组
11. 拓展参考资料
11.1 官方文档链接
MongoDB官方文档 - 数组操作符
- 链接:https://www.mongodb.com/docs/manual/reference/operator/query-array/
- 说明:详细介绍所有数组查询操作符的语法和用法
MongoDB官方文档 - 数组更新操作符
- 链接:https://www.mongodb.com/docs/manual/reference/operator/update-array/
- 说明:详细介绍所有数组更新操作符的语法和用法
MongoDB官方文档 - 多键索引
- 链接:https://www.mongodb.com/docs/manual/core/index-multikey/
- 说明:详细介绍多键索引的原理和使用方法
MongoDB官方文档 - 聚合管道
- 链接:https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
- 说明:详细介绍聚合管道的使用,包括数组的
$unwind、$group等操作
PHP MongoDB驱动官方文档
- 链接:https://www.php.net/manual/en/set.mongodb.php
- 说明:PHP MongoDB驱动的官方文档,包含详细的API说明
11.2 进阶学习路径建议
基础阶段
- 掌握数组的基本CRUD操作
- 理解数组查询操作符的使用场景
- 熟悉数组更新操作符的语法
进阶阶段
- 学习多键索引的原理和优化
- 掌握聚合管道中的数组操作
- 理解数组的位置操作符
高级阶段
- 学习数组性能优化技巧
- 掌握大规模数组数据处理
- 理解数组在分布式环境下的行为
实战阶段
- 参与实际项目的数组数据建模
- 解决生产环境中的数组性能问题
- 设计复杂的数组查询和更新逻辑
后续推荐学习:
- 《MongoDB聚合管道详解》
- 《MongoDB索引优化实战》
- 《MongoDB数据建模最佳实践》
- 《MongoDB性能调优指南》
