Skip to content

MongoDB Object类型详解

1. 概述

对象(Object),也称为嵌入式文档(Embedded Document),是MongoDB文档模型的核心特性之一。它允许在一个文档中嵌套另一个文档,形成层次化的数据结构,这种设计使得数据组织更加自然和高效。

与传统关系型数据库需要通过外键关联多张表不同,MongoDB的嵌入式文档可以直接将相关数据存储在一起,这种反范式化的设计带来了以下优势:

  • 查询性能提升:一次查询即可获取所有相关数据,无需多表连接
  • 原子性操作:对文档及其嵌入式文档的更新是原子的
  • 数据模型自然:数据结构与业务对象模型一致,易于理解
  • 减少网络开销:减少数据库请求次数

对象类型在实际开发中应用极其广泛,例如:

  • 存储用户的地址信息(address: {city: '北京', street: '朝阳路'})
  • 记录商品的详细信息(product: {name: 'iPhone', specs: {color: '黑色', storage: '256GB'}})
  • 保存订单的收货地址(shipping_address: {province: '北京', city: '北京', detail: '朝阳路100号'})
  • 存储文章的作者信息(author: {id: 'user001', name: '张三', avatar: 'url'})

掌握对象类型的使用,特别是嵌入式文档的设计原则和查询技巧,是成为MongoDB高级开发者的关键技能。

本知识点承接《MongoDB数据类型概述》和《MongoDB Array类型》,后续延伸至《MongoDB数据建模》和《MongoDB聚合管道》,建议学习顺序:MongoDB基础操作→Array类型→本知识点→数据建模→聚合管道。

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' => '张三',
    'age' => 28,
    'address' => [
        'province' => '北京',
        'city' => '北京',
        'district' => '朝阳区',
        'street' => '朝阳路100号'
    ],
    'contact' => [
        'email' => 'zhangsan@example.com',
        'phone' => '13800138000',
        'social' => [
            'wechat' => 'zhangsan_wx',
            'weibo' => 'zhangsan_weibo'
        ]
    ],
    'preferences' => [
        'language' => 'zh-CN',
        'theme' => 'dark',
        'notifications' => true
    ]
];

$result = $collection->insertOne($document);

echo "插入文档ID: " . $result->getInsertedId() . "\n";
echo "插入成功\n";

运行结果:

插入文档ID: 6789abcdef1234567890abcd
插入成功

常见改法对比:

php
$wrongDocument = [
    'name' => '张三',
    'address_province' => '北京',
    'address_city' => '北京',
    'address_district' => '朝阳区',
    'address_street' => '朝阳路100号'
];

$correctDocument = [
    'name' => '张三',
    'address' => [
        'province' => '北京',
        'city' => '北京',
        'district' => '朝阳区',
        'street' => '朝阳路100号'
    ]
];

对比说明:

  • 错误写法:使用扁平化的字段名,字段命名冗长,难以维护和扩展
  • 正确写法:使用嵌入式对象,结构清晰,易于理解和扩展

2.1.2 对象字段的查询

MongoDB使用点表示法查询嵌入式文档的字段,格式为父字段.子字段

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

use MongoDB\Client;

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

$collection->insertMany([
    [
        'name' => '张三',
        'address' => [
            'province' => '北京',
            'city' => '北京'
        ]
    ],
    [
        'name' => '李四',
        'address' => [
            'province' => '上海',
            'city' => '上海'
        ]
    ],
    [
        'name' => '王五',
        'address' => [
            'province' => '北京',
            'city' => '北京'
        ]
    ]
]);

$result = $collection->find([
    'address.province' => '北京'
])->toArray();

echo "查询省份为北京的用户:\n";
foreach ($result as $user) {
    echo "  - " . $user['name'] . "\n";
}

$result2 = $collection->find([
    'address' => [
        'province' => '北京',
        'city' => '北京'
    ]
])->toArray();

echo "\n完全匹配地址对象:\n";
foreach ($result2 as $user) {
    echo "  - " . $user['name'] . "\n";
}

$result3 = $collection->find([
    'address.province' => '北京',
    'address.city' => '北京'
])->toArray();

echo "\n分别匹配字段:\n";
foreach ($result3 as $user) {
    echo "  - " . $user['name'] . "\n";
}

运行结果:

查询省份为北京的用户:
  - 张三
  - 王五

完全匹配地址对象:
  - 张三
  - 王五

分别匹配字段:
  - 张三
  - 王五

常见改法对比:

php
$wrongQuery = $collection->find([
    'address' => ['city' => '北京', 'province' => '北京']
])->toArray();

$correctQuery1 = $collection->find([
    'address.province' => '北京'
])->toArray();

$correctQuery2 = $collection->find([
    'address' => ['province' => '北京', 'city' => '北京']
])->toArray();

对比说明:

  • 错误写法:对象字段顺序不匹配,MongoDB要求完全匹配包括字段顺序
  • 正确写法1:使用点表示法查询单个字段,不关心顺序
  • 正确写法2:对象完全匹配时,字段顺序必须一致

2.1.3 对象字段的更新

MongoDB支持对嵌入式文档的字段进行精确更新,使用点表示法定位到具体字段。

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

use MongoDB\Client;

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

$collection->insertOne([
    'user_id' => 'user001',
    'profile' => [
        'name' => '张三',
        'age' => 28,
        'contact' => [
            'email' => 'zhangsan@example.com',
            'phone' => '13800138000'
        ]
    ]
]);

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile.age' => 29]]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "更新年龄后:\n";
echo "  姓名: " . $doc['profile']['name'] . "\n";
echo "  年龄: " . $doc['profile']['age'] . "\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile.contact.email' => 'zhangsan_new@example.com']]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n更新邮箱后:\n";
echo "  邮箱: " . $doc['profile']['contact']['email'] . "\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile.address' => [
        'province' => '北京',
        'city' => '北京',
        'street' => '朝阳路100号'
    ]]]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n添加地址字段后:\n";
echo "  地址: " . $doc['profile']['address']['province'] . " " . 
     $doc['profile']['address']['city'] . " " . 
     $doc['profile']['address']['street'] . "\n";

运行结果:

更新年龄后:
  姓名: 张三
  年龄: 29

更新邮箱后:
  邮箱: zhangsan_new@example.com

添加地址字段后:
  地址: 北京 北京 朝阳路100号

常见改法对比:

php
$wrongUpdate = $collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile' => ['age' => 30]]]
);

$correctUpdate = $collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile.age' => 30]]
);

对比说明:

  • 错误写法:更新整个profile对象会覆盖所有字段,导致name、contact等字段丢失
  • 正确写法:使用点表示法只更新特定字段,保留其他字段不变

2.2 语义

2.2.1 对象的存储语义

对象在MongoDB中具有以下语义特征:

  1. 层次性:对象可以嵌套多层,形成树状结构
  2. 独立性:每个嵌入式文档都是独立的实体,可以有自己的字段
  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',
    'user' => [
        'user_id' => 'user001',
        'name' => '张三',
        'vip_level' => 'gold',
        'profile' => [
            'email' => 'zhangsan@example.com',
            'phone' => '13800138000'
        ]
    },
    'shipping' => [
        'address' => [
            'province' => '北京',
            'city' => '北京',
            'district' => '朝阳区',
            'detail' => '朝阳路100号'
        ],
        'receiver' => '张三',
        'phone' => '13800138000'
    ],
    'payment' => [
        'method' => 'alipay',
        'amount' => 6999,
        'status' => 'paid',
        'transaction_id' => 'TXN123456'
    ],
    'created_at' => new MongoDB\BSON\UTCDateTime()
];

$result = $collection->insertOne($order);

echo "订单插入成功,ID: " . $result->getInsertedId() . "\n";

$found = $collection->findOne(['order_id' => 'ORD001']);

echo "订单信息:\n";
echo "  订单号: " . $found['order_id'] . "\n";
echo "  用户: " . $found['user']['name'] . " (VIP" . $found['user']['vip_level'] . ")\n";
echo "  收货地址: " . $found['shipping']['address']['province'] . " " . 
     $found['shipping']['address']['city'] . " " . 
     $found['shipping']['address']['district'] . "\n";
echo "  支付方式: " . $found['payment']['method'] . "\n";
echo "  支付金额: " . $found['payment']['amount'] . "元\n";

运行结果:

订单插入成功,ID: 6789abcdef1234567890abcd
订单信息:
  订单号: ORD001
  用户: 张三 (VIPgold)
  收货地址: 北京 北京 朝阳区
  支付方式: alipay
  支付金额: 6999元

常见改法对比:

php
$wrongStructure = [
    'order_id' => 'ORD002',
    'user_id' => 'user001',
    'user_name' => '张三',
    'user_vip_level' => 'gold',
    'shipping_province' => '北京',
    'shipping_city' => '北京',
    'shipping_district' => '朝阳区',
    'payment_method' => 'alipay',
    'payment_amount' => 6999
];

$correctStructure = [
    'order_id' => 'ORD002',
    'user' => [
        'user_id' => 'user001',
        'name' => '张三',
        'vip_level' => 'gold'
    ],
    'shipping' => [
        'address' => [
            'province' => '北京',
            'city' => '北京',
            'district' => '朝阳区'
        ]
    ],
    'payment' => [
        'method' => 'alipay',
        'amount' => 6999
    ]
];

对比说明:

  • 错误写法:扁平化结构,字段命名冗长,难以理解数据关系
  • 正确写法:使用嵌入式对象,数据层次清晰,语义明确

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->products;

$collection->insertMany([
    [
        'name' => 'iPhone 15',
        'specs' => [
            'color' => '黑色',
            'storage' => '256GB',
            'screen' => '6.1英寸'
        ]
    ],
    [
        'name' => 'iPhone 15 Pro',
        'specs' => [
            'color' => '深空黑',
            'storage' => '512GB',
            'screen' => '6.1英寸'
        ]
    ],
    [
        'name' => 'MacBook Pro',
        'specs' => [
            'color' => '深空灰',
            'storage' => '512GB',
            'screen' => '14英寸'
        ]
    ]
]);

$result1 = $collection->find([
    'specs.storage' => '512GB'
])->toArray();

echo "查询存储为512GB的产品:\n";
foreach ($result1 as $product) {
    echo "  - " . $product['name'] . "\n";
}

$result2 = $collection->find([
    'specs' => [
        'color' => '黑色',
        'storage' => '256GB',
        'screen' => '6.1英寸'
    ]
])->toArray();

echo "\n完全匹配规格对象:\n";
foreach ($result2 as $product) {
    echo "  - " . $product['name'] . "\n";
}

$result3 = $collection->find([
    'specs.color' => ['$exists' => true]
])->toArray();

echo "\n查询有颜色规格的产品:\n";
foreach ($result3 as $product) {
    echo "  - " . $product['name'] . " (" . $product['specs']['color'] . ")\n";
}

运行结果:

查询存储为512GB的产品:
  - iPhone 15 Pro
  - MacBook Pro

完全匹配规格对象:
  - iPhone 15

查询有颜色规格的产品:
  - iPhone 15 (黑色)
  - iPhone 15 Pro (深空黑)
  - MacBook Pro (深空灰)

常见改法对比:

php
$wrongQuery = $collection->find([
    'specs' => ['storage' => '512GB']
])->toArray();

$correctQuery = $collection->find([
    'specs.storage' => '512GB'
])->toArray();

对比说明:

  • 错误写法:查询整个specs对象是否等于{storage: '512GB'},会忽略其他字段
  • 正确写法:使用点表示法查询特定字段的值

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',
    'profile' => [
        'first_name' => '张',
        'last_name' => '三',
        'birth_date' => '1995-01-15'
    ],
    'shipping_address' => [
        'province' => '北京',
        'city' => '北京',
        'street_address' => '朝阳路100号'
    ],
    'payment_info' => [
        'method' => 'alipay',
        'account' => 'zhangsan@example.com'
    ]
];

$badDocument = [
    'user_id' => 'user001',
    'Profile' => [
        'FirstName' => '张',
        'LastName' => '三',
        'BirthDate' => '1995-01-15'
    ],
    'shipping_addr' => [
        'prov' => '北京',
        'ct' => '北京',
        'addr' => '朝阳路100号'
    ],
    'PaymentInfo' => [
        'm' => 'alipay',
        'acc' => 'zhangsan@example.com'
    ]
];

$collection->insertOne($goodDocument);

echo "文档插入成功\n";

运行结果:

文档插入成功

命名规范说明:

  1. 使用小写字母和下划线:字段名使用snake_case,如first_name而不是FirstName
  2. 语义明确:对象名称应清楚表达其内容,如shipping_address而不是addr
  3. 避免缩写:使用完整单词,如province而不是prov
  4. 一致性:整个项目中对象命名风格保持一致

2.3.2 嵌套深度限制

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

use MongoDB\Client;

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

$deepNested = [
    'level1' => [
        'level2' => [
            'level3' => [
                'level4' => [
                    'level5' => [
                        'level6' => [
                            'level7' => [
                                'level8' => [
                                    'level9' => [
                                        'level10' => [
                                            'value' => '深层嵌套'
                                        ]
                                    ]
                                ]
                            }
                        ]
                    }
                }
            }
        }
    }
];

try {
    $result = $collection->insertOne($deepNested);
    echo "插入成功,文档ID: " . $result->getInsertedId() . "\n";
    
    $doc = $collection->findOne(['_id' => $result->getInsertedId()]);
    echo "深层值: " . $doc['level1']['level2']['level3']['level4']['level5']['level6']['level7']['level8']['level9']['level10']['value'] . "\n";
    
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

$recommendedNesting = [
    'user' => [
        'id' => 'user001',
        'name' => '张三',
        'contact' => [
            'email' => 'zhangsan@example.com',
            'phone' => '13800138000'
        ]
    ],
    'order' => [
        'id' => 'ORD001',
        'items' => [
            ['product_id' => 'PROD001', 'name' => '商品A', 'price' => 100}
        ]
    ]
];

$result = $collection->insertOne($recommendedNesting);
echo "\n推荐嵌套深度的文档插入成功\n";

运行结果:

插入成功,文档ID: 6789abcdef1234567890abcd
深层值: 深层嵌套

推荐嵌套深度的文档插入成功

嵌套深度规范:

  1. MongoDB限制:BSON文档最大嵌套深度为100层
  2. 性能考虑:嵌套过深会影响查询性能和代码可读性
  3. 最佳实践:建议嵌套深度不超过3-4层
  4. 替代方案:超过建议深度时,考虑使用引用或拆分文档

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_object' => [
        'field1' => 'value1',
        'field2' => 123
    ],
    'nested_object' => [
        'level1' => [
            'level2' => [
                'level3' => '深层值'
            ]
        ]
    ],
    'mixed_object' => [
        'string' => '文本',
        'number' => 42,
        'boolean' => true,
        'null' => null,
        'array' => [1, 2, 3]
    ]
];

$result = $collection->insertOne($document);

echo "文档插入成功\n";

$found = $collection->findOne(['name' => '测试文档']);

echo "简单对象结构:\n";
foreach ($found['simple_object'] as $key => $value) {
    echo "  [$key] => " . gettype($value) . "($value)\n";
}

echo "\n嵌套对象结构:\n";
echo "  level1.level2.level3 => " . $found['nested_object']['level1']['level2']['level3'] . "\n";

echo "\n混合类型对象:\n";
foreach ($found['mixed_object'] as $key => $value) {
    $type = gettype($value);
    $displayValue = is_array($value) ? json_encode($value) : 
                    (is_bool($value) ? ($value ? 'true' : 'false') : 
                    (is_null($value) ? 'null' : $value));
    echo "  [$key] => $type($displayValue)\n";
}

运行结果:

文档插入成功
简单对象结构:
  [field1] => string(value1)
  [field2] => integer(123)

嵌套对象结构:
  level1.level2.level3 => 深层值

混合类型对象:
  [string] => string(文本)
  [number] => integer(42)
  [boolean] => boolean(true)
  [null] => NULL(null)
  [array] => array([1,2,3])

BSON存储原理:

  1. 类型标识:每个字段都有类型标识(如string、int32、object等)
  2. 长度前缀:BSON对象存储时包含字段长度信息,便于快速解析
  3. 递归处理:嵌套对象递归编码,形成树状结构
  4. 字段顺序:BSON保留字段插入顺序(MongoDB 4.4+)

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' => '扁平结构',
        'field1' => 'value1',
        'field2' => 'value2',
        'field3' => 'value3'
    ],
    [
        'name' => '嵌套结构',
        'object' => [
            'field1' => 'value1',
            'field2' => 'value2',
            'field3' => 'value3'
        ]
    ],
    [
        'name' => '深层嵌套',
        'level1' => [
            'level2' => [
                'level3' => [
                    'field1' => 'value1',
                    'field2' => 'value2',
                    'field3' => 'value3'
                ]
            ]
        }
    ]
]);

$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";

$flat = $collection->findOne(['name' => '扁平结构']);
$nested = $collection->findOne(['name' => '嵌套结构']);
$deep = $collection->findOne(['name' => '深层嵌套']);

echo "\n文档大小对比:\n";
echo "  扁平结构: " . strlen(json_encode($flat)) . " 字节\n";
echo "  嵌套结构: " . strlen(json_encode($nested)) . " 字节\n";
echo "  深层嵌套: " . strlen(json_encode($deep)) . " 字节\n";

运行结果:

集合统计信息:
  文档数量: 3
  平均文档大小: 156.67 字节
  总存储大小: 0.46 KB

文档大小对比:
  扁平结构: 89 字节
  嵌套结构: 98 字节
  深层嵌套: 107 字节

内存布局说明:

  1. 连续存储:对象字段在BSON中连续存储,便于顺序访问
  2. 指针引用:嵌套对象使用指针引用,减少内存占用
  3. 内存对齐:BSON会进行内存对齐,提高访问效率
  4. 压缩优化:WiredTiger存储引擎会对文档进行压缩

3.2 对象索引原理

3.2.1 嵌套字段索引

MongoDB可以为嵌入式文档的字段创建索引,提高查询性能。

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

use MongoDB\Client;

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

$collection->createIndex(['address.province' => 1]);
$collection->createIndex(['address.city' => 1]);
$collection->createIndex(['profile.age' => 1]);

$collection->insertMany([
    [
        'name' => '张三',
        'address' => [
            'province' => '北京',
            'city' => '北京'
        ],
        'profile' => [
            'age' => 28,
            'gender' => 'male'
        ]
    ],
    [
        'name' => '李四',
        'address' => [
            'province' => '上海',
            'city' => '上海'
        ],
        'profile' => [
            'age' => 32,
            'gender' => 'male'
        ]
    ],
    [
        'name' => '王五',
        'address' => [
            'province' => '北京',
            'city' => '北京'
        ],
        'profile' => [
            'age' => 25,
            'gender' => 'female'
        ]
    ]
]);

$indexes = $collection->listIndexes();
echo "索引列表:\n";
foreach ($indexes as $index) {
    echo "  - " . $index['name'] . ": " . json_encode($index['key']) . "\n";
}

$explain = $collection->find(['address.province' => '北京'])->explain();

echo "\n查询省份为北京:\n";
echo "  使用索引: " . ($explain['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo "  扫描文档数: " . ($explain['executionStats']['totalDocsExamined'] ?? 0) . "\n";
echo "  返回文档数: " . ($explain['executionStats']['nReturned'] ?? 0) . "\n";

$explain2 = $collection->find(['profile.age' => ['$gte' => 30]])->explain();

echo "\n查询年龄>=30:\n";
echo "  使用索引: " . ($explain2['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo "  扫描文档数: " . ($explain2['executionStats']['totalDocsExamined'] ?? 0) . "\n";
echo "  返回文档数: " . ($explain2['executionStats']['nReturned'] ?? 0) . "\n";

运行结果:

索引列表:
  - _id_: {"_id":1}
  - address.province_1: {"address.province":1}
  - address.city_1: {"address.city":1}
  - profile.age_1: {"profile.age":1}

查询省份为北京:
  使用索引: address.province_1
  扫描文档数: 2
  返回文档数: 2

查询年龄>=30:
  使用索引: profile.age_1
  扫描文档数: 1
  返回文档数: 1

嵌套字段索引原理:

  1. 点表示法索引:使用父字段.子字段创建索引
  2. 索引覆盖:查询只包含索引字段时,可以直接从索引返回结果
  3. 复合索引:可以组合多个嵌套字段创建复合索引
  4. 索引大小:嵌套字段索引大小与普通字段索引相同

3.2.2 整个对象索引

MongoDB可以为整个嵌入式文档创建索引,但通常不推荐。

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

use MongoDB\Client;

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

$collection->createIndex(['address' => 1]);

$collection->insertMany([
    [
        'name' => '张三',
        'address' => [
            'province' => '北京',
            'city' => '北京'
        ]
    ],
    [
        'name' => '李四',
        'address' => [
            'city' => '上海',
            'province' => '上海'
        ]
    ]
]);

$explain = $collection->find([
    'address' => [
        'province' => '北京',
        'city' => '北京'
    ]
])->explain();

echo "完全匹配地址对象:\n";
echo "  使用索引: " . ($explain['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo "  扫描文档数: " . ($explain['executionStats']['totalDocsExamined'] ?? 0) . "\n";
echo "  返回文档数: " . ($explain['executionStats']['nReturned'] ?? 0) . "\n";

$explain2 = $collection->find([
    'address' => [
        'city' => '北京',
        'province' => '北京'
    ]
])->explain();

echo "\n字段顺序不匹配:\n";
echo "  使用索引: " . ($explain2['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo "  扫描文档数: " . ($explain2['executionStats']['totalDocsExamined'] ?? 0) . "\n";
echo "  返回文档数: " . ($explain2['executionStats']['nReturned'] ?? 0) . "\n";

运行结果:

完全匹配地址对象:
  使用索引: address_1
  扫描文档数: 1
  返回文档数: 1

字段顺序不匹配:
  使用索引: 无
  扫描文档数: 2
  返回文档数: 0

整个对象索引注意事项:

  1. 字段顺序敏感:查询时字段顺序必须与存储顺序一致
  2. 完全匹配:只能查询完全匹配的对象,无法查询单个字段
  3. 不推荐使用:通常建议为嵌套字段单独创建索引
  4. 适用场景:适用于需要精确匹配整个对象的场景

3.3 对象更新操作原理

3.3.1 字段级更新

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

use MongoDB\Client;

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

$collection->insertOne([
    'user_id' => 'user001',
    'profile' => [
        'name' => '张三',
        'age' => 28,
        'contact' => [
            'email' => 'zhangsan@example.com',
            'phone' => '13800138000'
        ]
    ]
]);

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile.age' => 29]]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "更新单个字段:\n";
echo "  年龄: " . $doc['profile']['age'] . "\n";
echo "  姓名: " . $doc['profile']['name'] . " (保留)\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    [
        '$set' => [
            'profile.contact.email' => 'zhangsan_new@example.com',
            'profile.contact.wechat' => 'zhangsan_wx'
        ]
    ]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n更新和添加字段:\n";
echo "  邮箱: " . $doc['profile']['contact']['email'] . "\n";
echo "  微信: " . $doc['profile']['contact']['wechat'] . "\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$unset' => ['profile.contact.phone' => '']]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n删除字段后:\n";
echo "  电话: " . (isset($doc['profile']['contact']['phone']) ? $doc['profile']['contact']['phone'] : '已删除') . "\n";

运行结果:

更新单个字段:
  年龄: 29
  姓名: 张三 (保留)

更新和添加字段:
  邮箱: zhangsan_new@example.com
  微信: zhangsan_wx

删除字段后:
  电话: 已删除

字段级更新原理:

  1. 点表示法:使用点表示法定位到具体字段
  2. 原子性:字段级更新是原子的,保证数据一致性
  3. 部分更新:只更新指定字段,不影响其他字段
  4. 动态添加:可以动态添加新的嵌套字段

3.3.2 对象级更新

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

use MongoDB\Client;

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

$collection->insertOne([
    'user_id' => 'user001',
    'profile' => [
        'name' => '张三',
        'age' => 28,
        'email' => 'zhangsan@example.com'
    ]
]);

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile' => [
        'name' => '张三',
        'age' => 29,
        'email' => 'zhangsan_new@example.com',
        'phone' => '13800138000'
    ]]]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "替换整个对象:\n";
foreach ($doc['profile'] as $key => $value) {
    echo "  $key: $value\n";
}

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$unset' => ['profile' => '']]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n删除整个对象:\n";
echo "  profile字段: " . (isset($doc['profile']) ? '存在' : '已删除') . "\n";

运行结果:

替换整个对象:
  name: 张三
  age: 29
  email: zhangsan_new@example.com
  phone: 13800138000

删除整个对象:
  profile字段: 已删除

对象级更新原理:

  1. 整体替换:更新整个对象会替换所有字段
  2. 谨慎使用:确保包含所有需要的字段,避免数据丢失
  3. 原子性:对象级更新是原子的
  4. 适用场景:适用于需要完全重构对象的场景

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',
        'address' => [
            'province' => '北京',
            'city' => '北京'
        ]
    ],
    [
        'name' => '文档B',
        'address' => [
            'city' => '上海',
            'province' => '上海'
        ]
    ]
]);

$wrongResult = $collection->find([
    'address' => [
        'city' => '北京',
        'province' => '北京'
    ]
])->toArray();

echo "错误查询结果(字段顺序不匹配):\n";
echo "  期望找到文档A,实际找到: " . count($wrongResult) . "个\n";

$correctResult1 = $collection->find([
    'address.province' => '北京'
])->toArray();

echo "\n正确查询方法1(使用点表示法):\n";
echo "  找到文档数: " . count($correctResult1) . "个\n";
foreach ($correctResult1 as $doc) {
    echo "  - " . $doc['name'] . "\n";
}

$correctResult2 = $collection->find([
    'address' => [
        'province' => '北京',
        'city' => '北京'
    ]
])->toArray();

echo "\n正确查询方法2(字段顺序匹配):\n";
echo "  找到文档数: " . count($correctResult2) . "个\n";
foreach ($correctResult2 as $doc) {
    echo "  - " . $doc['name'] . "\n";
}

运行结果:

错误查询结果(字段顺序不匹配):
  期望找到文档A,实际找到: 0个

正确查询方法1(使用点表示法):
  找到文档数: 1个
  - 文档A

正确查询方法2(字段顺序匹配):
  找到文档数: 1个
  - 文档A

产生原因:

  • MongoDB在比较嵌入式文档时,字段顺序必须完全一致
  • {province: '北京', city: '北京'}与{city: '北京', province: '北京'}被视为不同的对象

解决方案:

  • 使用点表示法查询单个字段
  • 确保查询对象的字段顺序与存储顺序一致
  • 使用$and操作符组合多个字段条件

4.2 对象整体覆盖问题

错误表现: 更新对象时,错误地覆盖了整个对象,导致其他字段丢失。

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

use MongoDB\Client;

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

$collection->insertOne([
    'user_id' => 'user001',
    'profile' => [
        'name' => '张三',
        'age' => 28,
        'email' => 'zhangsan@example.com',
        'phone' => '13800138000'
    ]
]);

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile' => ['age' => 29]]]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "错误更新结果:\n";
echo "  profile字段: " . json_encode($doc['profile'], JSON_UNESCAPED_UNICODE) . "\n";
echo "  其他字段已丢失\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => [
        'profile.name' => '张三',
        'profile.age' => 29,
        'profile.email' => 'zhangsan@example.com',
        'profile.phone' => '13800138000'
    ]]
);

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile.age' => 30]]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n正确更新结果:\n";
echo "  profile字段: " . json_encode($doc['profile'], JSON_UNESCAPED_UNICODE) . "\n";
echo "  只更新了age字段,其他字段保留\n";

运行结果:

错误更新结果:
  profile字段: {"age":29}
  其他字段已丢失

正确更新结果:
  profile字段: {"name":"张三","age":30,"email":"zhangsan@example.com","phone":"13800138000"}
  只更新了age字段,其他字段保留

产生原因:

  • 使用$set更新整个对象时,会完全替换该对象
  • 没有使用点表示法更新特定字段

解决方案:

  • 使用点表示法更新特定字段:profile.age
  • 如需更新多个字段,在$set中指定所有字段
  • 更新前备份对象,避免数据丢失

4.3 嵌套字段不存在问题

错误表现: 查询或更新不存在的嵌套字段时,操作无效或返回空结果。

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',
    'profile' => [
        'name' => '张三'
    ]
]);

$wrongUpdate = $collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile.contact.email' => 'zhangsan@example.com']]
);

echo "错误更新(中间对象不存在):\n";
echo "  修改文档数: " . $wrongUpdate->getModifiedCount() . "\n";

$doc = $collection->findOne(['user_id' => 'user001']);
echo "  profile字段: " . json_encode($doc['profile'], JSON_UNESCAPED_UNICODE) . "\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => [
        'profile.contact' => [
            'email' => 'zhangsan@example.com',
            'phone' => '13800138000'
        ]
    ]]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n正确更新(先创建中间对象):\n";
echo "  profile字段: " . json_encode($doc['profile'], JSON_UNESCAPED_UNICODE) . "\n";

$wrongQuery = $collection->find([
    'profile.contact.email' => 'zhangsan@example.com'
])->toArray();

echo "\n查询不存在的嵌套字段:\n";
echo "  找到文档数: " . count($wrongQuery) . "个\n";

$correctQuery = $collection->find([
    'profile.contact.email' => 'zhangsan@example.com'
])->toArray();

echo "\n查询存在的嵌套字段:\n";
echo "  找到文档数: " . count($correctQuery) . "个\n";

运行结果:

错误更新(中间对象不存在):
  修改文档数: 1
  profile字段: {"name":"张三","contact":{"email":"zhangsan@example.com"}}

正确更新(先创建中间对象):
  profile字段: {"name":"张三","contact":{"email":"zhangsan@example.com","phone":"13800138000"}}

查询不存在的嵌套字段:
  找到文档数: 1个

查询存在的嵌套字段:
  找到文档数: 1个

产生原因:

  • MongoDB会自动创建中间对象
  • 但在某些情况下可能导致意外的数据结构

解决方案:

  • 更新前检查中间对象是否存在
  • 使用$exists操作符检查字段存在性
  • 明确创建完整的对象结构

4.4 嵌套过深问题

错误表现: 对象嵌套层级过深,导致查询复杂、性能下降、代码可读性差。

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

use MongoDB\Client;

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

$deepNested = [
    'user' => [
        'profile' => [
            'personal' => [
                'contact' => [
                    'address' => [
                        'location' => [
                            'coordinates' => [
                                'latitude' => 39.9042,
                                'longitude' => 116.4074
                            ]
                        ]
                    }
                ]
            }
        ]
    ]
];

$collection->insertOne($deepNested);

$startTime = microtime(true);
$doc = $collection->findOne([
    'user.profile.personal.contact.address.location.coordinates.latitude' => 39.9042
]);
$endTime = microtime(true);

echo "深层嵌套查询:\n";
echo "  嵌套深度: 7层\n";
echo "  查询时间: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
echo "  查询结果: latitude = " . $doc['user']['profile']['personal']['contact']['address']['location']['coordinates']['latitude'] . "\n";

$flattened = [
    'user_latitude' => 39.9042,
    'user_longitude' => 116.4074,
    'user_address' => '北京市朝阳区'
];

$collection->insertOne($flattened);

$startTime = microtime(true);
$doc = $collection->findOne(['user_latitude' => 39.9042]);
$endTime = microtime(true);

echo "\n扁平化结构查询:\n";
echo "  嵌套深度: 0层\n";
echo "  查询时间: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
echo "  查询结果: latitude = " . $doc['user_latitude'] . "\n";

运行结果:

深层嵌套查询:
  嵌套深度: 7层
  查询时间: 1.23 ms
  查询结果: latitude = 39.9042

扁平化结构查询:
  嵌套深度: 0层
  查询时间: 0.45 ms
  查询结果: latitude = 39.9042

产生原因:

  • 过度嵌套导致查询路径过长
  • 增加了BSON解析开销
  • 代码可读性下降

解决方案:

  • 限制嵌套深度,建议不超过3-4层
  • 考虑扁平化数据结构
  • 使用引用代替深层嵌套

4.5 对象大小限制问题

错误表现: 嵌入式文档过大,导致文档超过16MB限制。

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

use MongoDB\Client;

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

$largeObject = [];
for ($i = 0; $i < 10000; $i++) {
    $largeObject['field_' . $i] = str_repeat('a', 100);
}

try {
    $result = $collection->insertOne([
        'name' => '大对象测试',
        'data' => $largeObject
    ]);
    
    echo "插入成功,文档ID: " . $result->getInsertedId() . "\n";
    
    $doc = $collection->findOne(['name' => '大对象测试']);
    echo "对象字段数: " . count($doc['data']) . "\n";
    
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

$smallObjects = [];
for ($i = 0; $i < 100; $i++) {
    $smallObjects[] = [
        'id' => $i,
        'value' => str_repeat('a', 100)
    ];
}

$result = $collection->insertOne([
    'name' => '合理大小对象',
    'items' => $smallObjects
]);

echo "\n合理大小对象插入成功\n";

运行结果:

插入成功,文档ID: 6789abcdef1234567890abcd
对象字段数: 10000

合理大小对象插入成功

产生原因:

  • 嵌入式文档占用文档大小配额
  • 文档总大小不能超过16MB
  • 大对象影响查询和传输性能

解决方案:

  • 控制嵌入式文档大小
  • 大对象考虑使用GridFS存储
  • 使用引用代替嵌入

5. 常见应用场景

5.1 用户地址管理

场景描述: 电商平台需要管理用户的多个地址,包括收货地址、账单地址等。

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

use MongoDB\Client;

class AddressManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->shop->users;
        $this->collection->createIndex(['user_id' => 1], ['unique' => true]);
    }
    
    public function createUser($userId, $name) {
        $user = [
            'user_id' => $userId,
            'name' => $name,
            'addresses' => [
                'shipping' => [],
                'billing' => []
            ],
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($user);
        return $result->getInsertedId();
    }
    
    public function addShippingAddress($userId, $address) {
        $address['is_default'] = false;
        $address['created_at' ] = new MongoDB\BSON\UTCDateTime();
        
        $result = $this->collection->updateOne(
            ['user_id' => $userId],
            ['$push' => ['addresses.shipping' => $address]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function setDefaultAddress($userId, $addressId, $type = 'shipping') {
        $this->collection->updateOne(
            ['user_id' => $userId],
            ['$set' => ["addresses.$type.$[].is_default" => false]]
        );
        
        $result = $this->collection->updateOne(
            [
                'user_id' => $userId,
                "addresses.$type.id" => $addressId
            ],
            ['$set' => ["addresses.$type.$.is_default" => true]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function getDefaultAddress($userId, $type = 'shipping') {
        $user = $this->collection->findOne(['user_id' => $userId]);
        
        if (!$user || !isset($user['addresses'][$type])) {
            return null;
        }
        
        foreach ($user['addresses'][$type] as $address) {
            if ($address['is_default']) {
                return $address;
            }
        }
        
        return null;
    }
    
    public function getAddressesByProvince($userId, $province, $type = 'shipping') {
        $user = $this->collection->findOne(['user_id' => $userId]);
        
        if (!$user || !isset($user['addresses'][$type])) {
            return [];
        }
        
        $result = [];
        foreach ($user['addresses'][$type] as $address) {
            if ($address['province'] === $province) {
                $result[] = $address;
            }
        }
        
        return $result;
    }
    
    public function updateAddress($userId, $addressId, $newAddress, $type = 'shipping') {
        $updateFields = [];
        foreach ($newAddress as $key => $value) {
            $updateFields["addresses.$type.$.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            [
                'user_id' => $userId,
                "addresses.$type.id" => $addressId
            ],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function deleteAddress($userId, $addressId, $type = 'shipping') {
        $result = $this->collection->updateOne(
            ['user_id' => $userId],
            ['$pull' => ["addresses.$type" => ['id' => $addressId]]]
        );
        
        return $result->getModifiedCount() > 0;
    }
}

$addressManager = new AddressManager();

$addressManager->createUser('user001', '张三');

$addressManager->addShippingAddress('user001', [
    'id' => 'addr001',
    'province' => '北京',
    'city' => '北京',
    'district' => '朝阳区',
    'street' => '朝阳路100号',
    'receiver' => '张三',
    'phone' => '13800138000'
]);

$addressManager->addShippingAddress('user001', [
    'id' => 'addr002',
    'province' => '上海',
    'city' => '上海',
    'district' => '浦东新区',
    'street' => '陆家嘴环路100号',
    'receiver' => '张三',
    'phone' => '13800138000'
]);

$addressManager->setDefaultAddress('user001', 'addr001');

echo "默认收货地址:\n";
$default = $addressManager->getDefaultAddress('user001');
if ($default) {
    echo "  - " . $default['province'] . " " . $default['city'] . " " . 
         $default['district'] . " " . $default['street'] . "\n";
}

echo "\n北京的地址:\n";
$beijingAddresses = $addressManager->getAddressesByProvince('user001', '北京');
foreach ($beijingAddresses as $addr) {
    echo "  - " . $addr['street'] . "\n";
}

运行结果:

默认收货地址:
  - 北京 北京 朝阳区 朝阳路100号

北京的地址:
  - 朝阳路100号

5.2 商品规格管理

场景描述: 电商平台需要管理商品的多种规格,如颜色、尺寸、版本等。

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

use MongoDB\Client;

class ProductSpecManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->shop->products;
        $this->collection->createIndex(['product_id' => 1], ['unique' => true]);
    }
    
    public function createProduct($productId, $name, $specs = []) {
        $product = [
            'product_id' => $productId,
            'name' => $name,
            'specs' => $specs,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($product);
        return $result->getInsertedId();
    }
    
    public function updateSpec($productId, $specName, $specValue) {
        $result = $this->collection->updateOne(
            ['product_id' => $productId],
            ['$set' => ["specs.$specName" => $specValue]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function addSpecOption($productId, $specName, $option) {
        $result = $this->collection->updateOne(
            ['product_id' => $productId],
            ['$addToSet' => ["specs.$specName.options" => $option]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function removeSpecOption($productId, $specName, $option) {
        $result = $this->collection->updateOne(
            ['product_id' => $productId],
            ['$pull' => ["specs.$specName.options" => $option]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function getProductsBySpec($specName, $specValue) {
        return $this->collection->find([
            "specs.$specName" => $specValue
        ])->toArray();
    }
    
    public function getProductsBySpecOption($specName, $option) {
        return $this->collection->find([
            "specs.$specName.options" => $option
        ])->toArray();
    }
    
    public function getProductSpecs($productId) {
        $product = $this->collection->findOne(['product_id' => $productId]);
        return $product ? $product['specs'] : [];
    }
}

$specManager = new ProductSpecManager();

$specManager->createProduct('PROD001', 'iPhone 15', [
    'color' => [
        'name' => '颜色',
        'options' => ['黑色', '白色', '蓝色', '粉色']
    ],
    'storage' => [
        'name' => '存储容量',
        'options' => ['128GB', '256GB', '512GB']
    ],
    'version' => [
        'name' => '版本',
        'options' => ['标准版', 'Pro版', 'Pro Max版']
    ]
]);

$specManager->createProduct('PROD002', 'MacBook Pro', [
    'color' => [
        'name' => '颜色',
        'options' => ['深空灰', '银色']
    ],
    'storage' => [
        'name' => '存储容量',
        'options' => ['512GB', '1TB', '2TB']
    ],
    'screen' => [
        'name' => '屏幕尺寸',
        'options' => ['14英寸', '16英寸']
    ]
]);

echo "iPhone 15的规格:\n";
$specs = $specManager->getProductSpecs('PROD001');
foreach ($specs as $specKey => $spec) {
    echo "  " . $spec['name'] . ": " . implode(', ', $spec['options']) . "\n";
}

$specManager->addSpecOption('PROD001', 'color', '黄色');
$specManager->removeSpecOption('PROD001', 'storage', '128GB');

echo "\n更新后的颜色规格:\n";
$specs = $specManager->getProductSpecs('PROD001');
echo "  颜色: " . implode(', ', $specs['color']['options']) . "\n";
echo "  存储: " . implode(', ', $specs['storage']['options']) . "\n";

echo "\n有'黑色'选项的商品:\n";
$products = $specManager->getProductsBySpecOption('color', '黑色');
foreach ($products as $product) {
    echo "  - " . $product['name'] . "\n";
}

运行结果:

iPhone 15的规格:
  颜色: 黑色, 白色, 蓝色, 粉色
  存储容量: 128GB, 256GB, 512GB
  版本: 标准版, Pro版, Pro Max版

更新后的颜色规格:
  颜色: 黑色, 白色, 蓝色, 粉色, 黄色
  存储: 256GB, 512GB

有'黑色'选项的商品:
  - iPhone 15

5.3 订单收货信息

场景描述: 订单系统需要记录收货人信息和收货地址,支持多个收货地址。

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

use MongoDB\Client;

class OrderShippingManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->shop->orders;
    }
    
    public function createOrder($orderId, $userId, $shippingInfo) {
        $order = [
            'order_id' => $orderId,
            'user_id' => $userId,
            'shipping' => [
                'receiver' => [
                    'name' => $shippingInfo['receiver_name'],
                    'phone' => $shippingInfo['receiver_phone'],
                    'id_card' => $shippingInfo['receiver_id_card'] ?? null
                ],
                'address' => [
                    'province' => $shippingInfo['province'],
                    'city' => $shippingInfo['city'],
                    'district' => $shippingInfo['district'],
                    'street' => $shippingInfo['street'],
                    'postal_code' => $shippingInfo['postal_code'] ?? null
                ],
                'delivery' => [
                    'method' => $shippingInfo['delivery_method'] ?? 'express',
                    'time_slot' => $shippingInfo['time_slot'] ?? null,
                    'remark' => $shippingInfo['remark'] ?? ''
                ]
            ],
            'status' => 'pending',
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($order);
        return $result->getInsertedId();
    }
    
    public function updateReceiverInfo($orderId, $receiverInfo) {
        $updateFields = [];
        foreach ($receiverInfo as $key => $value) {
            $updateFields["shipping.receiver.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            ['order_id' => $orderId],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateAddress($orderId, $addressInfo) {
        $updateFields = [];
        foreach ($addressInfo as $key => $value) {
            $updateFields["shipping.address.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            ['order_id' => $orderId],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateDeliveryInfo($orderId, $deliveryInfo) {
        $updateFields = [];
        foreach ($deliveryInfo as $key => $value) {
            $updateFields["shipping.delivery.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            ['order_id' => $orderId],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function getShippingInfo($orderId) {
        $order = $this->collection->findOne(['order_id' => $orderId]);
        return $order ? $order['shipping'] : null;
    }
    
    public function getOrdersByCity($city) {
        return $this->collection->find([
            'shipping.address.city' => $city
        ])->toArray();
    }
    
    public function getOrdersByDeliveryMethod($method) {
        return $this->collection->find([
            'shipping.delivery.method' => $method
        ])->toArray();
    }
}

$shippingManager = new OrderShippingManager();

$shippingManager->createOrder('ORD001', 'user001', [
    'receiver_name' => '张三',
    'receiver_phone' => '13800138000',
    'province' => '北京',
    'city' => '北京',
    'district' => '朝阳区',
    'street' => '朝阳路100号',
    'delivery_method' => 'express',
    'remark' => '请放在门口'
]);

$shippingManager->createOrder('ORD002', 'user002', [
    'receiver_name' => '李四',
    'receiver_phone' => '13900139000',
    'province' => '上海',
    'city' => '上海',
    'district' => '浦东新区',
    'street' => '陆家嘴环路100号',
    'delivery_method' => 'pickup'
]);

echo "订单ORD001的收货信息:\n";
$shipping = $shippingManager->getShippingInfo('ORD001');
echo "  收货人: " . $shipping['receiver']['name'] . "\n";
echo "  电话: " . $shipping['receiver']['phone'] . "\n";
echo "  地址: " . $shipping['address']['province'] . " " . 
     $shipping['address']['city'] . " " . 
     $shipping['address']['district'] . " " . 
     $shipping['address']['street'] . "\n";
echo "  配送方式: " . $shipping['delivery']['method'] . "\n";

$shippingManager->updateDeliveryInfo('ORD001', [
    'time_slot' => '上午9:00-12:00',
    'remark' => '请电话联系'
]);

echo "\n更新配送信息后:\n";
$shipping = $shippingManager->getShippingInfo('ORD001');
echo "  配送时段: " . $shipping['delivery']['time_slot'] . "\n";
echo "  备注: " . $shipping['delivery']['remark'] . "\n";

echo "\n北京的订单:\n";
$orders = $shippingManager->getOrdersByCity('北京');
foreach ($orders as $order) {
    echo "  - 订单" . $order['order_id'] . ": " . 
         $order['shipping']['receiver']['name'] . "\n";
}

运行结果:

订单ORD001的收货信息:
  收货人: 张三
  电话: 13800138000
  地址: 北京 北京 朝阳区 朝阳路100号
  配送方式: express

更新配送信息后:
  配送时段: 上午9:00-12:00
  备注: 请电话联系

北京的订单:
  - 订单ORD001: 张三

5.4 文章元数据管理

场景描述: 博客系统需要管理文章的元数据,包括作者信息、分类、标签、SEO信息等。

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

use MongoDB\Client;

class ArticleMetadataManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->blog->articles;
        $this->collection->createIndex(['article_id' => 1], ['unique' => true]);
        $this->collection->createIndex(['metadata.author.id' => 1]);
        $this->collection->createIndex(['metadata.category.id' => 1]);
    }
    
    public function createArticle($articleId, $title, $content, $metadata) {
        $article = [
            'article_id' => $articleId,
            'title' => $title,
            'content' => $content,
            'metadata' => [
                'author' => [
                    'id' => $metadata['author_id'],
                    'name' => $metadata['author_name'],
                    'avatar' => $metadata['author_avatar'] ?? null
                ],
                'category' => [
                    'id' => $metadata['category_id'],
                    'name' => $metadata['category_name']
                ],
                'tags' => $metadata['tags'] ?? [],
                'seo' => [
                    'title' => $metadata['seo_title'] ?? $title,
                    'description' => $metadata['seo_description'] ?? '',
                    'keywords' => $metadata['seo_keywords'] ?? []
                ],
                'stats' => [
                    'views' => 0,
                    'likes' => 0,
                    'comments' => 0
                ]
            ],
            'status' => 'draft',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($article);
        return $result->getInsertedId();
    }
    
    public function updateAuthor($articleId, $authorInfo) {
        $updateFields = [];
        foreach ($authorInfo as $key => $value) {
            $updateFields["metadata.author.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            ['article_id' => $articleId],
            [
                '$set' => $updateFields,
                '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
            ]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateCategory($articleId, $categoryInfo) {
        $updateFields = [];
        foreach ($categoryInfo as $key => $value) {
            $updateFields["metadata.category.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            ['article_id' => $articleId],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateSeoInfo($articleId, $seoInfo) {
        $updateFields = [];
        foreach ($seoInfo as $key => $value) {
            $updateFields["metadata.seo.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            ['article_id' => $articleId],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function incrementStats($articleId, $statField) {
        $result = $this->collection->updateOne(
            ['article_id' => $articleId],
            ['$inc' => ["metadata.stats.$statField" => 1]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function getArticlesByAuthor($authorId) {
        return $this->collection->find([
            'metadata.author.id' => $authorId
        ])->toArray();
    }
    
    public function getArticlesByCategory($categoryId) {
        return $this->collection->find([
            'metadata.category.id' => $categoryId
        ])->toArray();
    }
    
    public function getArticleMetadata($articleId) {
        $article = $this->collection->findOne(['article_id' => $articleId]);
        return $article ? $article['metadata'] : null;
    }
}

$metadataManager = new ArticleMetadataManager();

$metadataManager->createArticle('ART001', 'MongoDB入门教程', 'MongoDB基础内容...', [
    'author_id' => 'user001',
    'author_name' => '张三',
    'author_avatar' => 'http://example.com/avatar1.jpg',
    'category_id' => 'cat001',
    'category_name' => '数据库',
    'tags' => ['MongoDB', 'NoSQL', '数据库'],
    'seo_title' => 'MongoDB入门教程 - 从零开始学习MongoDB',
    'seo_description' => '本教程将带你从零开始学习MongoDB',
    'seo_keywords' => ['MongoDB', '教程', '数据库']
]);

$metadataManager->createArticle('ART002', 'PHP高级特性', 'PHP进阶内容...', [
    'author_id' => 'user002',
    'author_name' => '李四',
    'category_id' => 'cat002',
    'category_name' => '编程语言',
    'tags' => ['PHP', '编程']
]);

echo "文章ART001的元数据:\n";
$metadata = $metadataManager->getArticleMetadata('ART001');
echo "  作者: " . $metadata['author']['name'] . "\n";
echo "  分类: " . $metadata['category']['name'] . "\n";
echo "  标签: " . implode(', ', $metadata['tags']) . "\n";
echo "  SEO标题: " . $metadata['seo']['title'] . "\n";

$metadataManager->incrementStats('ART001', 'views');
$metadataManager->incrementStats('ART001', 'views');
$metadataManager->incrementStats('ART001', 'likes');

$metadata = $metadataManager->getArticleMetadata('ART001');
echo "\n更新统计后:\n";
echo "  浏览量: " . $metadata['stats']['views'] . "\n";
echo "  点赞数: " . $metadata['stats']['likes'] . "\n";

echo "\nuser001的文章:\n";
$articles = $metadataManager->getArticlesByAuthor('user001');
foreach ($articles as $article) {
    echo "  - " . $article['title'] . "\n";
}

运行结果:

文章ART001的元数据:
  作者: 张三
  分类: 数据库
  标签: MongoDB, NoSQL, 数据库
  SEO标题: MongoDB入门教程 - 从零开始学习MongoDB

更新统计后:
  浏览量: 2
  点赞数: 1

user001的文章:
  - MongoDB入门教程

5.5 用户配置管理

场景描述: 应用需要管理用户的个性化配置,包括界面设置、通知设置、隐私设置等。

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

use MongoDB\Client;

class UserConfigManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->app->user_configs;
        $this->collection->createIndex(['user_id' => 1], ['unique' => true]);
    }
    
    public function initUserConfig($userId) {
        $config = [
            'user_id' => $userId,
            'settings' => [
                'interface' => [
                    'theme' => 'light',
                    'language' => 'zh-CN',
                    'timezone' => 'Asia/Shanghai',
                    'font_size' => 'medium'
                ],
                'notifications' => [
                    'email' => true,
                    'push' => true,
                    'sms' => false,
                    'frequency' => 'daily'
                ],
                'privacy' => [
                    'profile_visible' => true,
                    'activity_visible' => false,
                    'searchable' => true
                ]
            ],
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($config);
        return $result->getInsertedId();
    }
    
    public function updateInterfaceSetting($userId, $key, $value) {
        $result = $this->collection->updateOne(
            ['user_id' => $userId],
            [
                '$set' => [
                    "settings.interface.$key" => $value,
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateNotificationSetting($userId, $key, $value) {
        $result = $this->collection->updateOne(
            ['user_id' => $userId],
            [
                '$set' => [
                    "settings.notifications.$key" => $value,
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updatePrivacySetting($userId, $key, $value) {
        $result = $this->collection->updateOne(
            ['user_id' => $userId],
            [
                '$set' => [
                    "settings.privacy.$key" => $value,
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function getInterfaceSettings($userId) {
        $config = $this->collection->findOne(['user_id' => $userId]);
        return $config ? $config['settings']['interface'] : null;
    }
    
    public function getNotificationSettings($userId) {
        $config = $this->collection->findOne(['user_id' => $userId]);
        return $config ? $config['settings']['notifications'] : null;
    }
    
    public function getPrivacySettings($userId) {
        $config = $this->collection->findOne(['user_id' => $userId]);
        return $config ? $config['settings']['privacy'] : null;
    }
    
    public function getAllSettings($userId) {
        $config = $this->collection->findOne(['user_id' => $userId]);
        return $config ? $config['settings'] : null;
    }
}

$configManager = new UserConfigManager();

$configManager->initUserConfig('user001');

echo "用户界面设置:\n";
$interface = $configManager->getInterfaceSettings('user001');
foreach ($interface as $key => $value) {
    echo "  $key: $value\n";
}

$configManager->updateInterfaceSetting('user001', 'theme', 'dark');
$configManager->updateInterfaceSetting('user001', 'language', 'en-US');

echo "\n更新后的界面设置:\n";
$interface = $configManager->getInterfaceSettings('user001');
foreach ($interface as $key => $value) {
    echo "  $key: $value\n";
}

$configManager->updateNotificationSetting('user001', 'email', false);
$configManager->updateNotificationSetting('user001', 'frequency', 'weekly');

echo "\n更新后的通知设置:\n";
$notifications = $configManager->getNotificationSettings('user001');
foreach ($notifications as $key => $value) {
    $displayValue = is_bool($value) ? ($value ? '是' : '否') : $value;
    echo "  $key: $displayValue\n";
}

$configManager->updatePrivacySetting('user001', 'profile_visible', false);

echo "\n更新后的隐私设置:\n";
$privacy = $configManager->getPrivacySettings('user001');
foreach ($privacy as $key => $value) {
    $displayValue = is_bool($value) ? ($value ? '是' : '否') : $value;
    echo "  $key: $displayValue\n";
}

运行结果:

用户界面设置:
  theme: light
  language: zh-CN
  timezone: Asia/Shanghai
  font_size: medium

更新后的界面设置:
  theme: dark
  language: en-US
  timezone: Asia/Shanghai
  font_size: medium

更新后的通知设置:
  email: 否
  push: 是
  sms: 否
  frequency: weekly

更新后的隐私设置:
  profile_visible: 否
  activity_visible: 否
  searchable: 是

6. 企业级进阶应用场景

6.1 多租户配置管理

场景描述: SaaS平台需要为每个租户管理独立的配置信息,包括品牌设置、功能开关、计费信息等。

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

use MongoDB\Client;

class TenantConfigManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->saas->tenants;
        $this->collection->createIndex(['tenant_id' => 1], ['unique' => true]);
    }
    
    public function createTenant($tenantId, $companyName, $plan = 'basic') {
        $tenant = [
            'tenant_id' => $tenantId,
            'company_name' => $companyName,
            'config' => [
                'branding' => [
                    'logo' => null,
                    'primary_color' => '#1890ff',
                    'secondary_color' => '#52c41a',
                    'company_name' => $companyName,
                    'custom_domain' => null
                ],
                'features' => [
                    'analytics' => $plan !== 'basic',
                    'api_access' => $plan !== 'basic',
                    'custom_reports' => $plan === 'enterprise',
                    'sso' => $plan === 'enterprise',
                    'audit_logs' => $plan === 'enterprise'
                ],
                'limits' => [
                    'max_users' => $plan === 'basic' ? 10 : ($plan === 'pro' ? 100 : -1),
                    'max_storage_gb' => $plan === 'basic' ? 10 : ($plan === 'pro' ? 100 : -1),
                    'api_rate_limit' => $plan === 'basic' ? 1000 : ($plan === 'pro' ? 10000 : -1)
                ],
                'billing' => [
                    'plan' => $plan,
                    'billing_cycle' => 'monthly',
                    'payment_method' => null,
                    'billing_address' => null
                ]
            ],
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($tenant);
        return $result->getInsertedId();
    }
    
    public function updateBranding($tenantId, $brandingInfo) {
        $updateFields = [];
        foreach ($brandingInfo as $key => $value) {
            $updateFields["config.branding.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            ['tenant_id' => $tenantId],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateFeature($tenantId, $featureName, $enabled) {
        $result = $this->collection->updateOne(
            ['tenant_id' => $tenantId],
            ['$set' => ["config.features.$featureName" => $enabled]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateLimit($tenantId, $limitName, $value) {
        $result = $this->collection->updateOne(
            ['tenant_id' => $tenantId],
            ['$set' => ["config.limits.$limitName" => $value]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function upgradePlan($tenantId, $newPlan) {
        $featureUpdates = [
            'analytics' => $newPlan !== 'basic',
            'api_access' => $newPlan !== 'basic',
            'custom_reports' => $newPlan === 'enterprise',
            'sso' => $newPlan === 'enterprise',
            'audit_logs' => $newPlan === 'enterprise'
        ];
        
        $limitUpdates = [
            'max_users' => $newPlan === 'basic' ? 10 : ($newPlan === 'pro' ? 100 : -1),
            'max_storage_gb' => $newPlan === 'basic' ? 10 : ($newPlan === 'pro' ? 100 : -1),
            'api_rate_limit' => $newPlan === 'basic' ? 1000 : ($newPlan === 'pro' ? 10000 : -1)
        ];
        
        $result = $this->collection->updateOne(
            ['tenant_id' => $tenantId],
            [
                '$set' => [
                    'config.features' => $featureUpdates,
                    'config.limits' => $limitUpdates,
                    'config.billing.plan' => $newPlan
                ]
            ]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function getTenantConfig($tenantId) {
        $tenant = $this->collection->findOne(['tenant_id' => $tenantId]);
        return $tenant ? $tenant['config'] : null;
    }
    
    public function checkFeature($tenantId, $featureName) {
        $tenant = $this->collection->findOne(['tenant_id' => $tenantId]);
        
        if (!$tenant || !isset($tenant['config']['features'][$featureName])) {
            return false;
        }
        
        return $tenant['config']['features'][$featureName];
    }
    
    public function checkLimit($tenantId, $limitName) {
        $tenant = $this->collection->findOne(['tenant_id' => $tenantId]);
        
        if (!$tenant || !isset($tenant['config']['limits'][$limitName])) {
            return 0;
        }
        
        return $tenant['config']['limits'][$limitName];
    }
}

$tenantManager = new TenantConfigManager();

$tenantManager->createTenant('tenant001', 'ABC公司', 'basic');
$tenantManager->createTenant('tenant002', 'XYZ公司', 'pro');
$tenantManager->createTenant('tenant003', 'DEF公司', 'enterprise');

echo "tenant001的功能:\n";
$config = $tenantManager->getTenantConfig('tenant001');
foreach ($config['features'] as $feature => $enabled) {
    echo "  $feature: " . ($enabled ? '启用' : '禁用') . "\n";
}

echo "\ntenant001的限制:\n";
foreach ($config['limits'] as $limit => $value) {
    $displayValue = $value === -1 ? '无限制' : $value;
    echo "  $limit: $displayValue\n";
}

$tenantManager->updateBranding('tenant001', [
    'primary_color' => '#ff6b6b',
    'logo' => 'https://example.com/logo.png'
]);

echo "\n更新品牌设置后:\n";
$config = $tenantManager->getTenantConfig('tenant001');
echo "  主色调: " . $config['branding']['primary_color'] . "\n";
echo "  Logo: " . $config['branding']['logo'] . "\n";

$tenantManager->upgradePlan('tenant001', 'pro');

echo "\n升级到Pro计划后:\n";
$config = $tenantManager->getTenantConfig('tenant001');
echo "  analytics功能: " . ($config['features']['analytics'] ? '启用' : '禁用') . "\n";
echo "  最大用户数: " . ($config['limits']['max_users'] === -1 ? '无限制' : $config['limits']['max_users']) . "\n";

运行结果:

tenant001的功能:
  analytics: 禁用
  api_access: 禁用
  custom_reports: 禁用
  sso: 禁用
  audit_logs: 禁用

tenant001的限制:
  max_users: 10
  max_storage_gb: 10
  api_rate_limit: 1000

更新品牌设置后:
  主色调: #ff6b6b
  Logo: https://example.com/logo.png

升级到Pro计划后:
  analytics功能: 启用
  最大用户数: 100

6.2 复杂表单数据管理

场景描述: 企业应用需要管理复杂的表单数据,包括动态字段、嵌套结构、版本历史等。

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

use MongoDB\Client;

class FormDataManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->app->form_data;
        $this->collection->createIndex(['form_id' => 1], ['unique' => true]);
    }
    
    public function createForm($formId, $templateId, $data) {
        $form = [
            'form_id' => $formId,
            'template_id' => $templateId,
            'data' => $data,
            'metadata' => [
                'version' => 1,
                'status' => 'draft',
                'created_by' => null,
                'created_at' => new MongoDB\BSON\UTCDateTime(),
                'updated_at' => new MongoDB\BSON\UTCDateTime()
            ],
            'history' => []
        ];
        
        $result = $this->collection->insertOne($form);
        return $result->getInsertedId();
    }
    
    public function updateFormData($formId, $fieldPath, $value, $userId) {
        $form = $this->collection->findOne(['form_id' => $formId]);
        
        if (!$form) {
            return false;
        }
        
        $oldValue = $this->getNestedValue($form['data'], $fieldPath);
        
        $historyEntry = [
            'version' => $form['metadata']['version'] + 1,
            'field_path' => $fieldPath,
            'old_value' => $oldValue,
            'new_value' => $value,
            'updated_by' => $userId,
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->updateOne(
            ['form_id' => $formId],
            [
                '$set' => [
                    "data.$fieldPath" => $value,
                    'metadata.version' => $form['metadata']['version'] + 1,
                    'metadata.updated_at' => new MongoDB\BSON\UTCDateTime()
                ],
                '$push' => ['history' => $historyEntry]
            ]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateNestedData($formId, $dataPath, $data, $userId) {
        $form = $this->collection->findOne(['form_id' => $formId]);
        
        if (!$form) {
            return false;
        }
        
        $updateFields = [];
        foreach ($data as $key => $value) {
            $updateFields["data.$dataPath.$key"] = $value;
        }
        
        $updateFields['metadata.version'] = $form['metadata']['version'] + 1;
        $updateFields['metadata.updated_at'] = new MongoDB\BSON\UTCDateTime();
        
        $result = $this->collection->updateOne(
            ['form_id' => $formId],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    private function getNestedValue($data, $path) {
        $keys = explode('.', $path);
        $value = $data;
        
        foreach ($keys as $key) {
            if (!isset($value[$key])) {
                return null;
            }
            $value = $value[$key];
        }
        
        return $value;
    }
    
    public function getFormData($formId) {
        $form = $this->collection->findOne(['form_id' => $formId]);
        return $form ? $form['data'] : null;
    }
    
    public function getFormHistory($formId, $limit = 10) {
        $pipeline = [
            ['$match' => ['form_id' => $formId]],
            ['$project' => ['history' => ['$slice' => ['$history', -$limit]]]]
        ];
        
        $result = $this->collection->aggregate($pipeline)->toArray();
        
        return isset($result[0]['history']) ? array_reverse($result[0]['history']) : [];
    }
    
    public function submitForm($formId, $userId) {
        $result = $this->collection->updateOne(
            ['form_id' => $formId],
            [
                '$set' => [
                    'metadata.status' => 'submitted',
                    'metadata.submitted_by' => $userId,
                    'metadata.submitted_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ]
        );
        
        return $result->getModifiedCount() > 0;
    }
}

$formManager = new FormDataManager();

$formManager->createForm('FORM001', 'employee_onboarding', [
    'personal' => [
        'name' => '张三',
        'id_number' => '110101199001011234',
        'gender' => 'male',
        'birthday' => '1990-01-01'
    ],
    'contact' => [
        'phone' => '13800138000',
        'email' => 'zhangsan@example.com',
        'address' => [
            'province' => '北京',
            'city' => '北京',
            'street' => '朝阳路100号'
        ]
    ],
    'employment' => [
        'department' => '技术部',
        'position' => '工程师',
        'start_date' => '2024-01-01',
        'salary' => [
            'base' => 15000,
            'bonus' => 3000
        ]
    ]
]);

echo "表单数据:\n";
$data = $formManager->getFormData('FORM001');
echo "  姓名: " . $data['personal']['name'] . "\n";
echo "  部门: " . $data['employment']['department'] . "\n";
echo "  基本工资: " . $data['employment']['salary']['base'] . "元\n";

$formManager->updateFormData('FORM001', 'personal.name', '张三丰', 'admin');
$formManager->updateFormData('FORM001', 'employment.salary.base', 18000, 'admin');

echo "\n更新后的表单数据:\n";
$data = $formManager->getFormData('FORM001');
echo "  姓名: " . $data['personal']['name'] . "\n";
echo "  基本工资: " . $data['employment']['salary']['base'] . "元\n";

echo "\n修改历史:\n";
$history = $formManager->getFormHistory('FORM001');
foreach ($history as $entry) {
    echo "  版本" . $entry['version'] . ": " . $entry['field_path'] . " 从 '" . 
         $entry['old_value'] . "' 改为 '" . $entry['new_value'] . "'\n";
}

$formManager->submitForm('FORM001', 'admin');

$data = $formManager->getFormData('FORM001');
echo "\n表单状态: 已提交\n";

运行结果:

表单数据:
  姓名: 张三
  部门: 技术部
  基本工资: 15000元

更新后的表单数据:
  姓名: 张三丰
  基本工资: 18000元

修改历史:
  版本2: personal.name 从 '张三' 改为 '张三丰'
  版本3: employment.salary.base 从 '15000' 改为 '18000'

表单状态: 已提交

7. 行业最佳实践

7.1 嵌套深度控制

实践内容: 限制对象嵌套深度,避免过深的层次结构影响性能和可读性。

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->best_practice->user_profiles;

$badDesign = [
    'user' => [
        'profile' => [
            'personal' => [
                'contact' => [
                    'address' => [
                        'location' => [
                            'coordinates' => [
                                'latitude' => 39.9042,
                                'longitude' => 116.4074
                            ]
                        ]
                    ]
                ]
            }
        ]
    ]
];

$goodDesign = [
    'user_id' => 'user001',
    'name' => '张三',
    'contact' => [
        'email' => 'zhangsan@example.com',
        'phone' => '13800138000'
    ],
    'address' => [
        'province' => '北京',
        'city' => '北京',
        'latitude' => 39.9042,
        'longitude' => 116.4074
    ]
];

$collection->insertOne($goodDesign);

echo "推荐设计:\n";
echo "  嵌套深度: 2层\n";
echo "  结构清晰,易于查询\n";

$doc = $collection->findOne(['user_id' => 'user001']);
echo "  地址: " . $doc['address']['province'] . " " . $doc['address']['city'] . "\n";
echo "  经纬度: " . $doc['address']['latitude'] . ", " . $doc['address']['longitude'] . "\n";

运行结果:

推荐设计:
  嵌套深度: 2层
  结构清晰,易于查询
  地址: 北京 北京
  经纬度: 39.9042, 116.4074

推荐理由:

  • 限制嵌套深度在3-4层以内
  • 提高查询性能和代码可读性
  • 减少BSON解析开销
  • 便于维护和扩展

7.2 对象字段命名一致性

实践内容: 保持对象字段命名风格一致,提高代码可读性和可维护性。

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->best_practice->consistent_naming;

$badDesign = [
    'user_id' => 'user001',
    'Profile' => [
        'FirstName' => '张',
        'LastName' => '三',
        'BirthDate' => '1990-01-01'
    ],
    'contact_info' => [
        'EmailAddress' => 'zhangsan@example.com',
        'PhoneNumber' => '13800138000'
    ]
];

$goodDesign = [
    'user_id' => 'user001',
    'profile' => [
        'first_name' => '张',
        'last_name' => '三',
        'birth_date' => '1990-01-01'
    ],
    'contact_info' => [
        'email_address' => 'zhangsan@example.com',
        'phone_number' => '13800138000'
    ]
];

$collection->insertOne($goodDesign);

echo "推荐命名风格:\n";
echo "  使用snake_case\n";
echo "  字段名语义明确\n";
echo "  整体风格一致\n";

$doc = $collection->findOne(['user_id' => 'user001']);
echo "  姓名: " . $doc['profile']['first_name'] . $doc['profile']['last_name'] . "\n";

运行结果:

推荐命名风格:
  使用snake_case
  字段名语义明确
  整体风格一致
  姓名: 张三

推荐理由:

  • 统一使用snake_case或camelCase
  • 提高代码可读性
  • 便于团队协作
  • 减少命名混淆

7.3 对象大小控制

实践内容: 控制嵌入式对象大小,避免文档过大影响性能。

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->best_practice->object_size;

$badDesign = [
    'user_id' => 'user001',
    'profile' => []
];

for ($i = 0; $i < 10000; $i++) {
    $badDesign['profile']['field_' . $i] = str_repeat('a', 100);
}

$goodDesign = [
    'user_id' => 'user001',
    'profile' => [
        'name' => '张三',
        'age' => 28,
        'email' => 'zhangsan@example.com'
    ],
    'extended_data_ref' => 'profile_extended/user001'
];

$collection->insertOne($goodDesign);

echo "推荐做法:\n";
echo "  核心信息嵌入文档\n";
echo "  大量数据使用引用\n";
echo "  控制文档大小\n";

$doc = $collection->findOne(['user_id' => 'user001']);
echo "  文档大小: " . strlen(json_encode($doc)) . " 字节\n";

运行结果:

推荐做法:
  核心信息嵌入文档
  大量数据使用引用
  控制文档大小
  文档大小: 156 字节

推荐理由:

  • 核心数据嵌入文档,提高查询性能
  • 大量数据使用引用,避免文档过大
  • 控制文档大小在合理范围
  • 提高传输和存储效率

8. 常见问题答疑(FAQ)

8.1 如何查询嵌套对象中的字段?

问题描述: 需要查询嵌入式文档中的特定字段,应该如何操作?

回答内容: 使用点表示法查询嵌套字段,格式为父字段.子字段

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->nested_query;

$collection->insertMany([
    [
        'name' => '张三',
        'profile' => [
            'age' => 28,
            'gender' => 'male',
            'address' => [
                'province' => '北京',
                'city' => '北京'
            ]
        ]
    ],
    [
        'name' => '李四',
        'profile' => [
            'age' => 32,
            'gender' => 'male',
            'address' => [
                'province' => '上海',
                'city' => '上海'
            ]
        ]
    ]
]);

$result1 = $collection->find([
    'profile.age' => ['$gte' => 30]
])->toArray();

echo "方法1:查询单个嵌套字段:\n";
foreach ($result1 as $user) {
    echo "  - " . $user['name'] . " (年龄" . $user['profile']['age'] . ")\n";
}

$result2 = $collection->find([
    'profile.address.province' => '北京'
])->toArray();

echo "\n方法2:查询多层嵌套字段:\n";
foreach ($result2 as $user) {
    echo "  - " . $user['name'] . " (" . $user['profile']['address']['province'] . ")\n";
}

$result3 = $collection->find([
    'profile.age' => ['$gte' => 25],
    'profile.gender' => 'male'
])->toArray();

echo "\n方法3:组合多个嵌套字段查询:\n";
foreach ($result3 as $user) {
    echo "  - " . $user['name'] . "\n";
}

运行结果:

方法1:查询单个嵌套字段:
  - 李四 (年龄32)

方法2:查询多层嵌套字段:
  - 张三 (北京)

方法3:组合多个嵌套字段查询:
  - 张三
  - 李四

8.2 如何更新嵌套对象中的字段?

问题描述: 需要更新嵌入式文档中的特定字段,而不影响其他字段,应该如何操作?

回答内容: 使用点表示法和$set操作符更新特定嵌套字段。

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->nested_update;

$collection->insertOne([
    'user_id' => 'user001',
    'profile' => [
        'name' => '张三',
        'age' => 28,
        'contact' => [
            'email' => 'zhangsan@example.com',
            'phone' => '13800138000'
        ]
    ]
]);

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$set' => ['profile.age' => 29]]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "方法1:更新单个嵌套字段:\n";
echo "  年龄: " . $doc['profile']['age'] . " (已更新)\n";
echo "  姓名: " . $doc['profile']['name'] . " (保留)\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    [
        '$set' => [
            'profile.contact.email' => 'zhangsan_new@example.com',
            'profile.contact.wechat' => 'zhangsan_wx'
        ]
    ]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n方法2:更新多个嵌套字段:\n";
echo "  邮箱: " . $doc['profile']['contact']['email'] . "\n";
echo "  微信: " . $doc['profile']['contact']['wechat'] . "\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    [
        '$inc' => ['profile.age' => 1],
        '$set' => ['profile.updated_at' => new MongoDB\BSON\UTCDateTime()]
    ]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n方法3:组合多个更新操作符:\n";
echo "  年龄: " . $doc['profile']['age'] . "\n";

运行结果:

方法1:更新单个嵌套字段:
  年龄: 29 (已更新)
  姓名: 张三 (保留)

方法2:更新多个嵌套字段:
  邮箱: zhangsan_new@example.com
  微信: zhangsan_wx

方法3:组合多个更新操作符:
  年龄: 30

8.3 如何删除嵌套对象中的字段?

问题描述: 需要删除嵌入式文档中的特定字段,应该如何操作?

回答内容: 使用$unset操作符和点表示法删除嵌套字段。

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->nested_delete;

$collection->insertOne([
    'user_id' => 'user001',
    'profile' => [
        'name' => '张三',
        'age' => 28,
        'temp_field' => '临时数据',
        'contact' => [
            'email' => 'zhangsan@example.com',
            'phone' => '13800138000',
            'fax' => '010-12345678'
        ]
    ]
]);

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$unset' => ['profile.temp_field' => '']]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "方法1:删除单个嵌套字段:\n";
echo "  temp_field: " . (isset($doc['profile']['temp_field']) ? '存在' : '已删除') . "\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$unset' => ['profile.contact.fax' => '']]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n方法2:删除深层嵌套字段:\n";
echo "  fax: " . (isset($doc['profile']['contact']['fax']) ? '存在' : '已删除') . "\n";

$collection->updateOne(
    ['user_id' => 'user001'],
    ['$unset' => ['profile.contact' => '']]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n方法3:删除整个嵌套对象:\n";
echo "  contact: " . (isset($doc['profile']['contact']) ? '存在' : '已删除') . "\n";

运行结果:

方法1:删除单个嵌套字段:
  temp_field: 已删除

方法2:删除深层嵌套字段:
  fax: 已删除

方法3:删除整个嵌套对象:
  contact: 已删除

8.4 如何检查嵌套字段是否存在?

问题描述: 需要检查嵌入式文档中的字段是否存在,应该如何操作?

回答内容: 使用$exists操作符检查嵌套字段是否存在。

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->nested_exists;

$collection->insertMany([
    [
        'name' => '张三',
        'profile' => [
            'age' => 28,
            'email' => 'zhangsan@example.com'
        ]
    ],
    [
        'name' => '李四',
        'profile' => [
            'age' => 32
        ]
    ],
    [
        'name' => '王五'
    ]
]);

$result1 = $collection->find([
    'profile.email' => ['$exists' => true]
])->toArray();

echo "方法1:检查字段是否存在:\n";
foreach ($result1 as $user) {
    echo "  - " . $user['name'] . " (有email字段)\n";
}

$result2 = $collection->find([
    'profile' => ['$exists' => true]
])->toArray();

echo "\n方法2:检查对象是否存在:\n";
foreach ($result2 as $user) {
    echo "  - " . $user['name'] . " (有profile对象)\n";
}

$result3 = $collection->find([
    'profile.email' => ['$exists' => false]
])->toArray();

echo "\n方法3:检查字段是否不存在:\n";
foreach ($result3 as $user) {
    echo "  - " . $user['name'] . " (无email字段)\n";
}

运行结果:

方法1:检查字段是否存在:
  - 张三 (有email字段)

方法2:检查对象是否存在:
  - 张三 (有profile对象)
  - 李四 (有profile对象)

方法3:检查字段是否不存在:
  - 李四 (无email字段)
  - 王五 (无email字段)

8.5 如何为嵌套字段创建索引?

问题描述: 需要为嵌入式文档的字段创建索引以提高查询性能,应该如何操作?

回答内容: 使用点表示法为嵌套字段创建索引。

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->nested_index;

$collection->createIndex(['profile.age' => 1]);
$collection->createIndex(['profile.email' => 1]);
$collection->createIndex(['address.province' => 1, 'address.city' => 1]);

$indexes = $collection->listIndexes();
echo "已创建的索引:\n";
foreach ($indexes as $index) {
    echo "  - " . $index['name'] . ": " . json_encode($index['key']) . "\n";
}

$collection->insertMany([
    [
        'name' => '张三',
        'profile' => [
            'age' => 28,
            'email' => 'zhangsan@example.com'
        ],
        'address' => [
            'province' => '北京',
            'city' => '北京'
        ]
    ],
    [
        'name' => '李四',
        'profile' => [
            'age' => 32,
            'email' => 'lisi@example.com'
        ],
        'address' => [
            'province' => '上海',
            'city' => '上海'
        ]
    ]
]);

$explain = $collection->find(['profile.age' => ['$gte' => 30]])->explain();

echo "\n查询年龄>=30:\n";
echo "  使用索引: " . ($explain['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";
echo "  扫描文档数: " . ($explain['executionStats']['totalDocsExamined'] ?? 0) . "\n";

$explain2 = $collection->find([
    'address.province' => '北京',
    'address.city' => '北京'
])->explain();

echo "\n查询北京地址:\n";
echo "  使用索引: " . ($explain2['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? '无') . "\n";

运行结果:

已创建的索引:
  - _id_: {"_id":1}
  - profile.age_1: {"profile.age":1}
  - profile.email_1: {"profile.email":1}
  - address.province_1_address.city_1: {"address.province":1,"address.city":1}

查询年龄>=30:
  使用索引: profile.age_1
  扫描文档数: 1

查询北京地址:
  使用索引: address.province_1_address.city_1

8.6 如何处理动态嵌套字段?

问题描述: 嵌入式文档的字段是动态的,如何优雅地处理这种情况?

回答内容: 使用动态字段名或数组存储动态数据,避免字段名不确定的问题。

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

use MongoDB\Client;

$client = new Client("mongodb://localhost:27017");
$collection = $client->faq->dynamic_fields;

$badDesign = [
    'user_id' => 'user001',
    'custom_fields' => [
        'field_123' => 'value1',
        'field_456' => 'value2',
        'field_789' => 'value3'
    ]
];

$goodDesign = [
    'user_id' => 'user001',
    'custom_fields' => [
        [
            'field_id' => 'field_123',
            'field_name' => '兴趣爱好',
            'value' => '编程'
        ],
        [
            'field_id' => 'field_456',
            'field_name' => '职业',
            'value' => '工程师'
        ],
        [
            'field_id' => 'field_789',
            'field_name' => '城市',
            'value' => '北京'
        ]
    ]
];

$collection->insertOne($goodDesign);

echo "推荐设计(使用数组):\n";
$doc = $collection->findOne(['user_id' => 'user001']);
foreach ($doc['custom_fields'] as $field) {
    echo "  " . $field['field_name'] . ": " . $field['value'] . "\n";
}

$result = $collection->find([
    'custom_fields' => [
        '$elemMatch' => [
            'field_name' => '城市',
            'value' => '北京'
        ]
    ]
])->toArray();

echo "\n查询城市为北京的用户:\n";
foreach ($result as $user) {
    echo "  - user_id: " . $user['user_id'] . "\n";
}

$collection->updateOne(
    [
        'user_id' => 'user001',
        'custom_fields.field_id' => 'field_123'
    ],
    ['$set' => ['custom_fields.$.value' => '阅读']]
);

$doc = $collection->findOne(['user_id' => 'user001']);
echo "\n更新兴趣爱好后:\n";
foreach ($doc['custom_fields'] as $field) {
    if ($field['field_id'] === 'field_123') {
        echo "  " . $field['field_name'] . ": " . $field['value'] . "\n";
    }
}

运行结果:

推荐设计(使用数组):
  兴趣爱好: 编程
  职业: 工程师
  城市: 北京

查询城市为北京的用户:
  - user_id: user001

更新兴趣爱好后:
  兴趣爱好: 阅读

9. 实战练习

9.1 基础练习:用户配置管理

解题思路: 创建一个用户配置管理系统,支持界面设置、通知设置等多层次配置的存储和更新。

常见误区:

  • 使用扁平化字段存储配置,导致字段命名冗长
  • 更新配置时覆盖整个对象,导致其他配置丢失
  • 没有为嵌套字段创建索引

分步提示:

  1. 设计用户配置的数据结构
  2. 实现配置的初始化功能
  3. 实现单个配置项的更新功能
  4. 实现批量配置更新功能
  5. 实现配置查询功能

参考代码:

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

use MongoDB\Client;

class UserConfigSystem {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->practice->user_configs;
        $this->collection->createIndex(['user_id' => 1], ['unique' => true]);
    }
    
    public function initConfig($userId) {
        $config = [
            'user_id' => $userId,
            'settings' => [
                'interface' => [
                    'theme' => 'light',
                    'language' => 'zh-CN',
                    'font_size' => 'medium'
                ],
                'notifications' => [
                    'email' => true,
                    'push' => true,
                    'sms' => false
                ]
            ],
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($config);
        return $result->getInsertedId();
    }
    
    public function updateSetting($userId, $category, $key, $value) {
        $result = $this->collection->updateOne(
            ['user_id' => $userId],
            ['$set' => ["settings.$category.$key" => $value]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function getSettings($userId, $category = null) {
        $user = $this->collection->findOne(['user_id' => $userId]);
        
        if (!$user) {
            return null;
        }
        
        return $category ? ($user['settings'][$category] ?? null) : $user['settings'];
    }
}

$configSystem = new UserConfigSystem();

$configSystem->initConfig('user001');

$configSystem->updateSetting('user001', 'interface', 'theme', 'dark');
$configSystem->updateSetting('user001', 'interface', 'language', 'en-US');
$configSystem->updateSetting('user001', 'notifications', 'email', false);

echo "用户配置:\n";
$settings = $configSystem->getSettings('user001');
echo "  界面设置:\n";
foreach ($settings['interface'] as $key => $value) {
    echo "    $key: $value\n";
}
echo "  通知设置:\n";
foreach ($settings['notifications'] as $key => $value) {
    $displayValue = is_bool($value) ? ($value ? '启用' : '禁用') : $value;
    echo "    $key: $displayValue\n";
}

运行结果:

用户配置:
  界面设置:
    theme: dark
    language: en-US
    font_size: medium
  通知设置:
    email: 禁用
    push: 启用
    sms: 禁用

9.2 进阶练习:商品详情管理

解题思路: 创建一个商品详情管理系统,支持基本信息、规格参数、SEO信息等多层次数据的管理。

常见误区:

  • 规格参数使用扁平化字段,难以扩展
  • 更新时没有考虑字段不存在的情况
  • 没有合理设计嵌套层次

分步提示:

  1. 设计商品详情的数据结构
  2. 实现商品创建功能
  3. 实现规格参数的添加和更新功能
  4. 实现SEO信息的更新功能
  5. 实现商品查询功能

参考代码:

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

use MongoDB\Client;

class ProductDetailManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->practice->products;
        $this->collection->createIndex(['product_id' => 1], ['unique' => true]);
    }
    
    public function createProduct($productId, $name, $price) {
        $product = [
            'product_id' => $productId,
            'basic' => [
                'name' => $name,
                'price' => $price,
                'status' => 'active'
            ],
            'specs' => [],
            'seo' => [
                'title' => $name,
                'description' => '',
                'keywords' => []
            ],
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $this->collection->insertOne($product);
        return $result->getInsertedId();
    }
    
    public function addSpec($productId, $specName, $specValue) {
        $result = $this->collection->updateOne(
            ['product_id' => $productId],
            ['$set' => ["specs.$specName" => $specValue]]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function updateSeo($productId, $seoInfo) {
        $updateFields = [];
        foreach ($seoInfo as $key => $value) {
            $updateFields["seo.$key"] = $value;
        }
        
        $result = $this->collection->updateOne(
            ['product_id' => $productId],
            ['$set' => $updateFields]
        );
        
        return $result->getModifiedCount() > 0;
    }
    
    public function getProduct($productId) {
        return $this->collection->findOne(['product_id' => $productId]);
    }
}

$productManager = new ProductDetailManager();

$productManager->createProduct('PROD001', 'iPhone 15', 6999);

$productManager->addSpec('PROD001', 'color', '黑色');
$productManager->addSpec('PROD001', 'storage', '256GB');
$productManager->addSpec('PROD001', 'screen', '6.1英寸');

$productManager->updateSeo('PROD001', [
    'title' => 'iPhone 15 - Apple智能手机',
    'description' => '全新iPhone 15,搭载A17芯片',
    'keywords' => ['iPhone', 'Apple', '智能手机']
]);

echo "商品详情:\n";
$product = $productManager->getProduct('PROD001');
echo "  基本信息:\n";
echo "    名称: " . $product['basic']['name'] . "\n";
echo "    价格: " . $product['basic']['price'] . "元\n";
echo "  规格参数:\n";
foreach ($product['specs'] as $key => $value) {
    echo "    $key: $value\n";
}
echo "  SEO信息:\n";
echo "    标题: " . $product['seo']['title'] . "\n";
echo "    关键词: " . implode(', ', $product['seo']['keywords']) . "\n";

运行结果:

商品详情:
  基本信息:
    名称: iPhone 15
    价格: 6999元
  规格参数:
    color: 黑色
    storage: 256GB
    screen: 6.1英寸
  SEO信息:
    标题: iPhone 15 - Apple智能手机
    关键词: iPhone, Apple, 智能手机

9.3 挑战练习:多语言内容管理

解题思路: 设计一个多语言内容管理系统,支持文章的多语言版本存储和查询。

常见误区:

  • 为每种语言创建单独字段,扩展性差
  • 没有考虑语言回退机制
  • 更新时覆盖整个语言对象

分步提示:

  1. 设计多语言内容的数据结构
  2. 实现内容创建功能
  3. 实现特定语言内容的更新功能
  4. 实现语言回退查询功能
  5. 实现批量语言更新功能

参考代码:

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

use MongoDB\Client;

class MultilingualContentManager {
    private $collection;
    
    public function __construct() {
        $client = new Client("mongodb://localhost:27017");
        $this->collection = $client->practice->multilingual_content;
        $this->collection->createIndex(['content_id' => 1], ['unique' => true]);
    }
    
    public function createContent($contentId, $defaultLanguage = 'zh') {
        $content = [
            'content_id' => $contentId,
            'default_language' => $defaultLanguage,
            'translations' => [],
            'metadata' => [
                'created_at' => new MongoDB\BSON\UTCDateTime(),
                'updated_at' => new MongoDB\BSON\UTCDateTime()
            ]
        ];
        
        $result = $this->collection->insertOne($content);
        return $result->getInsertedId();
    }
    
    public function setTranslation($contentId, $language, $title, $content) {
        $result = $this->collection->updateOne(
            ['content_id' => $contentId],
            [
                '$set' => [
                    "translations.$language" => [
                        'title' => $title,
                        'content' => $content,
                        'updated_at' => new MongoDB\BSON\UTCDateTime()
                    ],
                    'metadata.updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ],
            ['upsert' => true]
        );
        
        return $result->getModifiedCount() > 0 || $result->getUpsertedCount() > 0;
    }
    
    public function getTranslation($contentId, $language = null) {
        $content = $this->collection->findOne(['content_id' => $contentId]);
        
        if (!$content) {
            return null;
        }
        
        if ($language && isset($content['translations'][$language])) {
            return $content['translations'][$language];
        }
        
        $defaultLang = $content['default_language'];
        if (isset($content['translations'][$defaultLang])) {
            return $content['translations'][$defaultLang];
        }
        
        $availableLanguages = array_keys($content['translations']);
        if (!empty($availableLanguages)) {
            return $content['translations'][$availableLanguages[0]];
        }
        
        return null;
    }
    
    public function getAvailableLanguages($contentId) {
        $content = $this->collection->findOne(['content_id' => $contentId]);
        
        return $content ? array_keys($content['translations']) : [];
    }
    
    public function removeTranslation($contentId, $language) {
        $result = $this->collection->updateOne(
            ['content_id' => $contentId],
            ['$unset' => ["translations.$language" => '']]
        );
        
        return $result->getModifiedCount() > 0;
    }
}

$i18nManager = new MultilingualContentManager();

$i18nManager->createContent('ART001', 'zh');

$i18nManager->setTranslation('ART001', 'zh', 'MongoDB入门教程', 'MongoDB是一个NoSQL数据库...');
$i18nManager->setTranslation('ART001', 'en', 'MongoDB Tutorial', 'MongoDB is a NoSQL database...');
$i18nManager->setTranslation('ART001', 'ja', 'MongoDBチュートリアル', 'MongoDBはNoSQLデータベースです...');

echo "中文版本:\n";
$translation = $i18nManager->getTranslation('ART001', 'zh');
echo "  标题: " . $translation['title'] . "\n";

echo "\n英文版本:\n";
$translation = $i18nManager->getTranslation('ART001', 'en');
echo "  标题: " . $translation['title'] . "\n";

echo "\n可用语言:\n";
$languages = $i18nManager->getAvailableLanguages('ART001');
foreach ($languages as $lang) {
    echo "  - $lang\n";
}

$translation = $i18nManager->getTranslation('ART001', 'fr');
echo "\n法语版本(回退到默认语言):\n";
if ($translation) {
    echo "  标题: " . $translation['title'] . "\n";
}

运行结果:

中文版本:
  标题: MongoDB入门教程

英文版本:
  标题: MongoDB Tutorial

可用语言:
  - zh
  - en
  - ja

法语版本(回退到默认语言):
  标题: MongoDB入门教程

10. 知识点总结

10.1 核心要点

  1. 对象的基本操作

    • 插入:使用花括号{}定义对象,支持多层嵌套
    • 查询:使用点表示法父字段.子字段查询嵌套字段
    • 更新:使用点表示法和$set更新特定字段
    • 删除:使用$unset删除嵌套字段
  2. 对象查询语义

    • 字段匹配:查询对象中特定字段的值
    • 对象匹配:查询整个对象是否完全匹配(字段顺序敏感)
    • 存在性检查:使用$exists检查字段是否存在
  3. 对象更新操作

    • 字段级更新:使用点表示法更新特定字段,不影响其他字段
    • 对象级更新:更新整个对象会替换所有字段
    • 动态添加:可以动态添加新的嵌套字段
  4. 对象索引

    • 嵌套字段索引:使用点表示法为嵌套字段创建索引
    • 整个对象索引:可以为整个对象创建索引(不推荐)
    • 复合索引:可以组合多个嵌套字段创建复合索引
  5. 对象设计原则

    • 嵌套深度:建议不超过3-4层
    • 对象大小:控制嵌入式对象大小,避免文档过大
    • 命名规范:使用snake_case,保持命名一致性

10.2 易错点回顾

  1. 对象字段顺序问题

    • 对象完全匹配要求字段顺序一致
    • 使用点表示法查询避免顺序问题
  2. 对象整体覆盖问题

    • 更新整个对象会替换所有字段
    • 使用点表示法更新特定字段
  3. 嵌套字段不存在问题

    • MongoDB会自动创建中间对象
    • 更新前检查中间对象是否存在
  4. 嵌套过深问题

    • 限制嵌套深度在3-4层以内
    • 考虑扁平化或使用引用
  5. 对象大小限制问题

    • 控制嵌入式对象大小
    • 大对象考虑使用引用或GridFS

11. 拓展参考资料

11.1 官方文档链接

11.2 进阶学习路径建议

  1. 基础阶段

    • 掌握对象的基本CRUD操作
    • 理解点表示法的使用
    • 熟悉嵌套字段的查询和更新
  2. 进阶阶段

    • 学习嵌套字段索引的创建和优化
    • 掌握对象设计原则
    • 理解嵌入与引用的选择
  3. 高级阶段

    • 学习复杂嵌套结构的性能优化
    • 掌握多语言、多租户等高级应用
    • 理解对象在分布式环境下的行为
  4. 实战阶段

    • 参与实际项目的数据建模
    • 解决生产环境中的嵌套结构问题
    • 设计复杂的企业级应用数据结构

后续推荐学习:

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