Skip to content

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中具有以下语义特征:

  1. 有序性:数组元素按照插入顺序存储,可以通过索引访问
  2. 可重复性:数组中的元素可以重复(除非使用$addToSet等操作符保证唯一性)
  3. 动态性:数组长度可以动态变化,不需要预先定义大小
  4. 嵌套性:数组可以嵌套对象或其他数组,形成复杂的数据结构
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 数组的查询语义

数组字段的查询有以下几种语义:

  1. 元素匹配:查询数组中是否包含某个元素
  2. 完全匹配:查询数组是否完全等于指定数组(包括顺序)
  3. 条件匹配:查询数组中是否有元素满足特定条件
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";

运行结果:

文档插入成功

命名规范说明:

  1. 使用复数形式:数组字段应使用复数名词,如tagscommentsitems
  2. 避免缩写:使用完整单词,如phone_numbers而不是phones
  3. 语义明确:名称应清楚表达数组内容,如order_items而不是items
  4. 一致性:整个项目中数组命名风格保持一致

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
推荐大小的数组插入成功

数组大小规范:

  1. 文档大小限制:MongoDB单个文档最大16MB,数组大小不能超过此限制
  2. 性能考虑:数组过大(如超过1000个元素)会影响查询和更新性能
  3. 索引限制:数组字段创建索引时,索引条目数量 = 文档数 × 数组元素数,需要控制数组大小
  4. 最佳实践:单个数组建议不超过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存储原理:

  1. 类型标识:每个数组元素都有类型标识(如int32、string、boolean等)
  2. 长度前缀:BSON数组存储时包含元素长度信息,便于快速解析
  3. 嵌套支持:数组可以嵌套对象或其他数组,BSON递归处理嵌套结构
  4. 索引优化: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 个元素

内存布局说明:

  1. 连续存储:数组元素在BSON中连续存储,便于顺序访问
  2. 指针引用:对于大对象或嵌套文档,使用指针引用减少内存占用
  3. 内存对齐:BSON会进行内存对齐,提高访问效率
  4. 压缩优化: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

多键索引原理:

  1. 索引展开:数组['MongoDB', '数据库', 'NoSQL']会创建3个索引条目
  2. 去重存储:如果数组中有重复元素,索引中只存储一个条目
  3. 查询优化:查询数组元素时,直接使用索引定位文档,无需全表扫描
  4. 覆盖索引:如果查询只包含索引字段,可以直接从索引返回结果

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

复合索引规则:

  1. 数组字段限制:复合索引中最多只能有一个数组字段
  2. 索引顺序:数组字段通常放在复合索引的后面
  3. 前缀匹配:复合索引支持前缀查询,如索引(a, b, c)支持查询a和(a, b)
  4. 索引选择: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操作原理:

  1. 追加操作:默认在数组末尾追加元素
  2. 原子性:$push操作是原子的,保证并发安全
  3. 修饰符
    • $each:批量添加多个元素
    • $position:指定插入位置
    • $slice:限制数组最大长度
    • $sort:添加后对数组排序
  4. 性能优化:对于大数组,使用$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操作原理:

  1. 条件删除:根据条件删除数组元素,支持复杂查询条件
  2. 批量删除:一次删除所有匹配的元素
  3. 嵌套匹配:对于对象数组,可以匹配对象的多个字段
  4. 原子性:$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 1

5.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赞)

评论总数: 3

5.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进行维护

标记第一条为已读后:
  未读数量: 2

6. 企业级进阶应用场景

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: 红色, M

8.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
  - 物理: 78

8.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, F

9. 实战练习

9.1 基础练习:博客标签管理

解题思路: 创建一个博客文章集合,实现标签的添加、删除、查询功能。需要使用$push$pull$addToSet等数组操作符。

常见误区:

  • 使用$push添加标签时没有检查重复
  • 删除标签时使用了错误的操作符
  • 查询标签时没有考虑数组匹配规则

分步提示:

  1. 创建文章集合并插入示例数据
  2. 实现添加标签功能(使用$addToSet避免重复)
  3. 实现删除标签功能(使用$pull
  4. 实现按标签查询文章功能(使用$in$all
  5. 实现获取热门标签功能(使用聚合管道)

参考代码:

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 进阶练习:订单商品管理

解题思路: 创建一个订单系统,订单包含多个商品项,实现商品的添加、删除、数量修改、总价计算等功能。需要使用数组的位置操作符和聚合管道。

常见误区:

  • 更新商品数量时没有使用位置操作符$
  • 计算总价时没有考虑商品数量
  • 删除商品后没有重新计算订单总价

分步提示:

  1. 创建订单集合并插入示例订单
  2. 实现添加商品功能(检查是否已存在)
  3. 实现更新商品数量功能(使用位置操作符)
  4. 实现删除商品功能
  5. 实现计算订单总价功能(使用聚合管道)

参考代码:

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 挑战练习:实时聊天系统

解题思路: 设计一个实时聊天系统,每个会话包含多条消息,支持消息发送、已读状态管理、消息搜索等功能。需要考虑性能优化和数据一致性。

常见误区:

  • 消息数组无限增长导致文档过大
  • 没有对消息进行分页处理
  • 更新已读状态时性能低下

分步提示:

  1. 设计会话和消息的数据结构
  2. 实现发送消息功能(使用$push$slice限制消息数量)
  3. 实现消息分页查询
  4. 实现已读状态管理(使用数组过滤操作符)
  5. 实现消息搜索功能(使用聚合管道)

参考代码:

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 核心要点

  1. 数组的基本操作

    • 插入:使用方括号[]定义数组,支持不同类型元素
    • 查询:使用点表示法、$in$all$elemMatch$size等操作符
    • 更新:使用$push$pull$addToSet$pop等操作符
    • 删除:使用$pull删除指定元素,$pop删除首尾元素
  2. 数组查询操作符

    • $in:匹配数组中任意一个元素
    • $all:匹配数组中所有指定元素(不关心顺序)
    • $elemMatch:匹配数组中满足多个条件的元素
    • $size:匹配指定长度的数组
  3. 数组更新操作符

    • $push:添加元素到数组末尾
    • $addToSet:添加唯一元素到数组
    • $pull:删除匹配条件的元素
    • $pop:删除首尾元素
    • $each:批量操作多个元素
    • $slice:限制数组长度
    • $sort:对数组排序
    • $position:指定插入位置
  4. 数组索引

    • 多键索引:数组字段自动创建多键索引
    • 索引展开:每个数组元素创建一个索引条目
    • 复合索引:复合索引中最多只能有一个数组字段
  5. 数组位置操作符

    • $:更新第一个匹配的元素
    • $[]:更新所有元素
    • $[<identifier>]:更新满足条件的元素

10.2 易错点回顾

  1. 数组完全匹配陷阱

    • 数组完全匹配要求元素顺序一致
    • 使用$all操作符匹配多个元素(不关心顺序)
  2. 数组元素条件查询错误

    • 使用$elemMatch确保同一个元素满足所有条件
    • 避免多个条件分散到不同元素
  3. 数组更新重复元素

    • 使用$addToSet代替$push避免重复元素
    • 需要重复元素时才使用$push
  4. 数组索引越界

    • MongoDB允许更新不存在的索引位置
    • 更新前检查数组长度,避免意外增长
  5. 大数组性能问题

    • 限制数组大小,建议不超过1000个元素
    • 使用$slice自动维护数组长度
    • 考虑分表或引用方式处理超大数组

11. 拓展参考资料

11.1 官方文档链接

11.2 进阶学习路径建议

  1. 基础阶段

    • 掌握数组的基本CRUD操作
    • 理解数组查询操作符的使用场景
    • 熟悉数组更新操作符的语法
  2. 进阶阶段

    • 学习多键索引的原理和优化
    • 掌握聚合管道中的数组操作
    • 理解数组的位置操作符
  3. 高级阶段

    • 学习数组性能优化技巧
    • 掌握大规模数组数据处理
    • 理解数组在分布式环境下的行为
  4. 实战阶段

    • 参与实际项目的数组数据建模
    • 解决生产环境中的数组性能问题
    • 设计复杂的数组查询和更新逻辑

后续推荐学习:

  • 《MongoDB聚合管道详解》
  • 《MongoDB索引优化实战》
  • 《MongoDB数据建模最佳实践》
  • 《MongoDB性能调优指南》