Skip to content

3.3 文档结构设计

概述

文档结构设计是MongoDB数据建模的核心,直接影响应用的性能、可维护性和扩展性。与关系型数据库的表结构设计不同,MongoDB的文档结构更加灵活,支持嵌套文档、数组、混合数据类型等特性。本章节将深入探讨MongoDB文档结构设计的原则、模式和实践技巧。

良好的文档结构设计需要考虑数据的访问模式、查询需求、更新频率、数据量大小等多个因素。通过合理的文档结构设计,可以优化查询性能、减少数据冗余、简化应用逻辑、提高系统的整体效率。

基本概念

文档结构定义

MongoDB文档是BSON格式的二进制表示,具有以下特点:

  • 键值对结构:文档由字段和值组成,类似于JSON对象
  • 嵌套能力:支持多层嵌套文档和数组
  • 动态模式:同一集合中的文档可以有不同的字段
  • 类型丰富:支持多种数据类型,包括字符串、数字、日期、二进制数据等
  • 大小限制:单个文档最大16MB

文档设计原则

  1. 嵌入与引用:根据数据关系选择嵌入或引用模式
  2. 数据原子性:考虑更新操作的原子性需求
  3. 查询性能:根据查询模式优化文档结构
  4. 数据增长:预测文档大小增长,避免超出限制
  5. 读写比例:根据读写比例选择合适的数据组织方式

常见文档模式

  1. 嵌入模式:将相关数据嵌入到同一文档中
  2. 引用模式:通过引用字段关联其他文档
  3. 混合模式:结合嵌入和引用的优点
  4. 桶模式:将时间序列数据分组存储
  5. 属性模式:使用对象数组存储可变属性

原理深度解析

嵌入模式原理

嵌入模式将相关数据存储在同一个文档中,减少查询次数:

php
<?php
// 嵌入模式示例 - 用户和订单信息
class EmbeddedDocumentPattern {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function createUserWithEmbeddedOrders($userData, $orders) {
        $collection = $this->database->selectCollection('users_embedded');
        
        $userDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'name' => $userData['name'],
            'email' => $userData['email'],
            'address' => [
                'street' => $userData['address']['street'],
                'city' => $userData['address']['city'],
                'state' => $userData['address']['state'],
                'zip' => $userData['address']['zip']
            ],
            'orders' => $orders,
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($userDocument);
        
        return [
            'user_id' => $result->getInsertedId(),
            'orders_count' => count($orders),
            'document_size' => strlen(json_encode($userDocument))
        ];
    }
    
    public function getUserWithOrders($userId) {
        $collection = $this->database->selectCollection('users_embedded');
        
        $user = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($userId)]);
        
        if ($user) {
            return [
                'user_id' => $user['_id'],
                'name' => $user['name'],
                'email' => $user['email'],
                'address' => $user['address'],
                'orders' => $user['orders'],
                'total_orders' => count($user['orders'])
            ];
        }
        
        return null;
    }
    
    public function addOrderToUser($userId, $orderData) {
        $collection = $this->database->selectCollection('users_embedded');
        
        $result = $collection->updateOne(
            ['_id' => new MongoDB\BSON\ObjectId($userId)],
            [
                '$push' => ['orders' => $orderData],
                '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
            ]
        );
        
        return [
            'modified_count' => $result->getModifiedCount(),
            'order_added' => $result->getModifiedCount() > 0
        ];
    }
    
    public function analyzeEmbeddedPattern($userId) {
        $collection = $this->database->selectCollection('users_embedded');
        
        $user = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($userId)]);
        
        if (!$user) {
            return ['error' => 'User not found'];
        }
        
        $documentSize = strlen(json_encode($user));
        $ordersCount = count($user['orders']);
        
        return [
            'document_size_bytes' => $documentSize,
            'document_size_mb' => round($documentSize / (1024 * 1024), 4),
            'orders_count' => $ordersCount,
            'avg_order_size' => $ordersCount > 0 ? round($documentSize / $ordersCount, 2) : 0,
            'size_warning' => $documentSize > 10 * 1024 * 1024, // 10MB warning
            'recommendation' => $documentSize > 10 * 1024 * 1024 ? 
                'Consider using reference pattern for orders' : 
                'Embedded pattern is appropriate'
        ];
    }
}

// 使用示例
$embeddedPattern = new EmbeddedDocumentPattern('testdb');

// 创建用户并嵌入订单
$userData = [
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'address' => [
        'street' => '123 Main St',
        'city' => 'New York',
        'state' => 'NY',
        'zip' => '10001'
    ]
];

$orders = [
    [
        'order_id' => 'ORD001',
        'date' => new MongoDB\BSON\UTCDateTime(),
        'total' => 99.99,
        'items' => [
            ['product_id' => 'P001', 'name' => 'Product 1', 'quantity' => 2, 'price' => 49.99]
        ]
    ],
    [
        'order_id' => 'ORD002',
        'date' => new MongoDB\BSON\UTCDateTime(),
        'total' => 149.99,
        'items' => [
            ['product_id' => 'P002', 'name' => 'Product 2', 'quantity' => 1, 'price' => 149.99]
        ]
    ]
];

$result = $embeddedPattern->createUserWithEmbeddedOrders($userData, $orders);
print_r($result);

// 获取用户和订单信息
$user = $embeddedPattern->getUserWithOrders($result['user_id']);
print_r($user);

// 添加新订单
$newOrder = [
    'order_id' => 'ORD003',
    'date' => new MongoDB\BSON\UTCDateTime(),
    'total' => 79.99,
    'items' => [
        ['product_id' => 'P003', 'name' => 'Product 3', 'quantity' => 1, 'price' => 79.99]
    ]
];

$addResult = $embeddedPattern->addOrderToUser($result['user_id'], $newOrder);
print_r($addResult);

// 分析嵌入模式
$analysis = $embeddedPattern->analyzeEmbeddedPattern($result['user_id']);
print_r($analysis);
?>

引用模式原理

引用模式通过引用字段关联不同文档,适合一对多关系:

php
<?php
// 引用模式示例 - 用户和订单分离存储
class ReferencedDocumentPattern {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function createUser($userData) {
        $collection = $this->database->selectCollection('users_referenced');
        
        $userDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'name' => $userData['name'],
            'email' => $userData['email'],
            'address' => [
                'street' => $userData['address']['street'],
                'city' => $userData['address']['city'],
                'state' => $userData['address']['state'],
                'zip' => $userData['address']['zip']
            ],
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($userDocument);
        
        return $result->getInsertedId();
    }
    
    public function createOrder($userId, $orderData) {
        $collection = $this->database->selectCollection('orders_referenced');
        
        $orderDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'user_id' => $userId,
            'order_number' => $orderData['order_number'],
            'date' => $orderData['date'],
            'total' => $orderData['total'],
            'items' => $orderData['items'],
            'status' => $orderData['status'] ?? 'pending',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($orderDocument);
        
        return $result->getInsertedId();
    }
    
    public function getUserWithOrders($userId) {
        $usersCollection = $this->database->selectCollection('users_referenced');
        $ordersCollection = $this->database->selectCollection('orders_referenced');
        
        // 获取用户信息
        $user = $usersCollection->findOne(['_id' => new MongoDB\BSON\ObjectId($userId)]);
        
        if (!$user) {
            return null;
        }
        
        // 获取用户的所有订单
        $orders = $ordersCollection->find(['user_id' => new MongoDB\BSON\ObjectId($userId)])->toArray();
        
        return [
            'user_id' => $user['_id'],
            'name' => $user['name'],
            'email' => $user['email'],
            'address' => $user['address'],
            'orders' => $orders,
            'total_orders' => count($orders)
        ];
    }
    
    public function getOrderWithUser($orderId) {
        $ordersCollection = $this->database->selectCollection('orders_referenced');
        $usersCollection = $this->database->selectCollection('users_referenced');
        
        // 获取订单信息
        $order = $ordersCollection->findOne(['_id' => new MongoDB\BSON\ObjectId($orderId)]);
        
        if (!$order) {
            return null;
        }
        
        // 获取用户信息
        $user = $usersCollection->findOne(['_id' => $order['user_id']]);
        
        return [
            'order_id' => $order['_id'],
            'order_number' => $order['order_number'],
            'date' => $order['date'],
            'total' => $order['total'],
            'items' => $order['items'],
            'status' => $order['status'],
            'user' => [
                'user_id' => $user['_id'],
                'name' => $user['name'],
                'email' => $user['email']
            ]
        ];
    }
    
    public function comparePatterns($userId) {
        $embeddedCollection = $this->database->selectCollection('users_embedded');
        $referencedUsersCollection = $this->database->selectCollection('users_referenced');
        $referencedOrdersCollection = $this->database->selectCollection('orders_referenced');
        
        // 分析嵌入模式
        $embeddedUser = $embeddedCollection->findOne(['_id' => new MongoDB\BSON\ObjectId($userId)]);
        $embeddedSize = strlen(json_encode($embeddedUser));
        
        // 分析引用模式
        $referencedUser = $referencedUsersCollection->findOne(['_id' => new MongoDB\BSON\ObjectId($userId)]);
        $referencedOrders = $referencedOrdersCollection->find(['user_id' => new MongoDB\BSON\ObjectId($userId)])->toArray();
        
        $userSize = strlen(json_encode($referencedUser));
        $ordersSize = strlen(json_encode($referencedOrders));
        $referencedTotalSize = $userSize + $ordersSize;
        
        return [
            'embedded_pattern' => [
                'document_size' => $embeddedSize,
                'query_count' => 1,
                'advantages' => ['Single query', 'Atomic updates', 'Better read performance'],
                'disadvantages' => ['Document size limit', 'Data duplication', 'Complex updates']
            ],
            'referenced_pattern' => [
                'total_size' => $referencedTotalSize,
                'query_count' => 2,
                'advantages' => ['No size limit', 'Data normalization', 'Flexible updates'],
                'disadvantages' => ['Multiple queries', 'Non-atomic updates', 'Complex joins']
            ],
            'recommendation' => $embeddedSize > 5 * 1024 * 1024 ? 
                'Use referenced pattern' : 
                'Use embedded pattern'
        ];
    }
}

// 使用示例
$referencedPattern = new ReferencedDocumentPattern('testdb');

// 创建用户
$userData = [
    'name' => 'Jane Smith',
    'email' => 'jane@example.com',
    'address' => [
        'street' => '456 Oak Ave',
        'city' => 'Los Angeles',
        'state' => 'CA',
        'zip' => '90001'
    ]
];

$userId = $referencedPattern->createUser($userData);
echo "Created user with ID: " . $userId . "\n";

// 创建订单
$orders = [
    [
        'order_number' => 'ORD101',
        'date' => new MongoDB\BSON\UTCDateTime(),
        'total' => 199.99,
        'items' => [
            ['product_id' => 'P101', 'name' => 'Product A', 'quantity' => 2, 'price' => 99.99]
        ],
        'status' => 'completed'
    ],
    [
        'order_number' => 'ORD102',
        'date' => new MongoDB\BSON\UTCDateTime(),
        'total' => 299.99,
        'items' => [
            ['product_id' => 'P102', 'name' => 'Product B', 'quantity' => 1, 'price' => 299.99]
        ],
        'status' => 'pending'
    ]
];

$orderIds = [];
foreach ($orders as $order) {
    $orderId = $referencedPattern->createOrder($userId, $order);
    $orderIds[] = $orderId;
    echo "Created order with ID: " . $orderId . "\n";
}

// 获取用户和订单信息
$userWithOrders = $referencedPattern->getUserWithOrders($userId);
print_r($userWithOrders);

// 获取订单和用户信息
$orderWithUser = $referencedPattern->getOrderWithUser($orderIds[0]);
print_r($orderWithUser);

// 比较两种模式
$comparison = $referencedPattern->comparePatterns($userId);
print_r($comparison);
?>

桶模式原理

桶模式将时间序列数据分组存储,优化查询性能:

php
<?php
// 桶模式示例 - 传感器数据分组存储
class BucketPattern {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function createBucket($sensorId, $timestamp, $bucketSize = 'hour') {
        $collection = $this->database->selectCollection('sensor_data_buckets');
        
        $bucketKey = $this->generateBucketKey($timestamp, $bucketSize);
        
        $bucketDocument = [
            '_id' => $sensorId . '_' . $bucketKey,
            'sensor_id' => $sensorId,
            'bucket_key' => $bucketKey,
            'bucket_size' => $bucketSize,
            'start_time' => $this->getBucketStartTime($timestamp, $bucketSize),
            'end_time' => $this->getBucketEndTime($timestamp, $bucketSize),
            'measurements' => [],
            'count' => 0,
            'min_value' => null,
            'max_value' => null,
            'avg_value' => 0,
            'sum_value' => 0,
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($bucketDocument);
        
        return $result->getInsertedId();
    }
    
    public function addMeasurement($sensorId, $timestamp, $value, $bucketSize = 'hour') {
        $collection = $this->database->selectCollection('sensor_data_buckets');
        
        $bucketKey = $this->generateBucketKey($timestamp, $bucketSize);
        $bucketId = $sensorId . '_' . $bucketKey;
        
        // 检查桶是否存在
        $bucket = $collection->findOne(['_id' => $bucketId]);
        
        if (!$bucket) {
            $this->createBucket($sensorId, $timestamp, $bucketSize);
            $bucket = $collection->findOne(['_id' => $bucketId]);
        }
        
        $measurement = [
            'timestamp' => $timestamp,
            'value' => $value
        ];
        
        // 更新桶数据
        $result = $collection->updateOne(
            ['_id' => $bucketId],
            [
                '$push' => ['measurements' => $measurement],
                '$inc' => [
                    'count' => 1,
                    'sum_value' => $value
                ],
                '$min' => ['min_value' => $value],
                '$max' => ['max_value' => $value],
                '$set' => [
                    'avg_value' => ($bucket['sum_value'] + $value) / ($bucket['count'] + 1),
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ]
        );
        
        return [
            'bucket_id' => $bucketId,
            'measurement_added' => $result->getModifiedCount() > 0,
            'new_count' => $bucket['count'] + 1
        ];
    }
    
    public function getBucketData($sensorId, $bucketKey) {
        $collection = $this->database->selectCollection('sensor_data_buckets');
        
        $bucket = $collection->findOne([
            'sensor_id' => $sensorId,
            'bucket_key' => $bucketKey
        ]);
        
        if (!$bucket) {
            return null;
        }
        
        return [
            'bucket_id' => $bucket['_id'],
            'sensor_id' => $bucket['sensor_id'],
            'bucket_key' => $bucket['bucket_key'],
            'start_time' => $bucket['start_time'],
            'end_time' => $bucket['end_time'],
            'measurements' => $bucket['measurements'],
            'statistics' => [
                'count' => $bucket['count'],
                'min_value' => $bucket['min_value'],
                'max_value' => $bucket['max_value'],
                'avg_value' => $bucket['avg_value'],
                'sum_value' => $bucket['sum_value']
            ]
        ];
    }
    
    public function queryTimeRange($sensorId, $startTime, $endTime, $bucketSize = 'hour') {
        $collection = $this->database->selectCollection('sensor_data_buckets');
        
        $startBucketKey = $this->generateBucketKey($startTime, $bucketSize);
        $endBucketKey = $this->generateBucketKey($endTime, $bucketSize);
        
        $buckets = $collection->find([
            'sensor_id' => $sensorId,
            'bucket_key' => ['$gte' => $startBucketKey, '$lte' => $endBucketKey]
        ])->toArray();
        
        $allMeasurements = [];
        $aggregatedStats = [
            'total_count' => 0,
            'global_min' => null,
            'global_max' => null,
            'global_avg' => 0,
            'global_sum' => 0
        ];
        
        foreach ($buckets as $bucket) {
            // 过滤时间范围内的测量数据
            $filteredMeasurements = array_filter($bucket['measurements'], function($measurement) use ($startTime, $endTime) {
                $timestamp = $measurement['timestamp'];
                return $timestamp >= $startTime && $timestamp <= $endTime;
            });
            
            $allMeasurements = array_merge($allMeasurements, $filteredMeasurements);
            
            // 更新聚合统计
            $aggregatedStats['total_count'] += count($filteredMeasurements);
            $aggregatedStats['global_sum'] += $bucket['sum_value'];
            
            if ($bucket['min_value'] !== null) {
                if ($aggregatedStats['global_min'] === null || $bucket['min_value'] < $aggregatedStats['global_min']) {
                    $aggregatedStats['global_min'] = $bucket['min_value'];
                }
            }
            
            if ($bucket['max_value'] !== null) {
                if ($aggregatedStats['global_max'] === null || $bucket['max_value'] > $aggregatedStats['global_max']) {
                    $aggregatedStats['global_max'] = $bucket['max_value'];
                }
            }
        }
        
        if ($aggregatedStats['total_count'] > 0) {
            $aggregatedStats['global_avg'] = $aggregatedStats['global_sum'] / $aggregatedStats['total_count'];
        }
        
        return [
            'sensor_id' => $sensorId,
            'time_range' => [
                'start' => $startTime,
                'end' => $endTime
            ],
            'buckets_queried' => count($buckets),
            'measurements' => $allMeasurements,
            'aggregated_statistics' => $aggregatedStats
        ];
    }
    
    private function generateBucketKey($timestamp, $bucketSize) {
        $dateTime = $timestamp->toDateTime();
        
        switch ($bucketSize) {
            case 'minute':
                return $dateTime->format('YmdHi');
            case 'hour':
                return $dateTime->format('YmdH');
            case 'day':
                return $dateTime->format('Ymd');
            case 'month':
                return $dateTime->format('Ym');
            default:
                return $dateTime->format('YmdH');
        }
    }
    
    private function getBucketStartTime($timestamp, $bucketSize) {
        $dateTime = clone $timestamp->toDateTime();
        
        switch ($bucketSize) {
            case 'minute':
                $dateTime->setTime($dateTime->format('H'), $dateTime->format('i'), 0);
                break;
            case 'hour':
                $dateTime->setTime($dateTime->format('H'), 0, 0);
                break;
            case 'day':
                $dateTime->setTime(0, 0, 0);
                break;
            case 'month':
                $dateTime->setDate($dateTime->format('Y'), $dateTime->format('m'), 1);
                $dateTime->setTime(0, 0, 0);
                break;
        }
        
        return new MongoDB\BSON\UTCDateTime($dateTime->getTimestamp() * 1000);
    }
    
    private function getBucketEndTime($timestamp, $bucketSize) {
        $startTime = $this->getBucketStartTime($timestamp, $bucketSize);
        $dateTime = clone $startTime->toDateTime();
        
        switch ($bucketSize) {
            case 'minute':
                $dateTime->modify('+1 minute');
                break;
            case 'hour':
                $dateTime->modify('+1 hour');
                break;
            case 'day':
                $dateTime->modify('+1 day');
                break;
            case 'month':
                $dateTime->modify('+1 month');
                break;
        }
        
        return new MongoDB\BSON\UTCDateTime($dateTime->getTimestamp() * 1000);
    }
}

// 使用示例
$bucketPattern = new BucketPattern('testdb');

$sensorId = 'sensor_001';
$baseTime = time() * 1000;

// 添加测量数据
for ($i = 0; $i < 100; $i++) {
    $timestamp = new MongoDB\BSON\UTCDateTime($baseTime + $i * 60000); // 每分钟一个数据点
    $value = 20 + rand(-5, 5);
    
    $result = $bucketPattern->addMeasurement($sensorId, $timestamp, $value, 'hour');
    echo "Added measurement: " . $result['new_count'] . "\n";
}

// 获取桶数据
$bucketKey = date('YmdH', $baseTime / 1000);
$bucketData = $bucketPattern->getBucketData($sensorId, $bucketKey);
print_r($bucketData);

// 查询时间范围数据
$startTime = new MongoDB\BSON\UTCDateTime($baseTime);
$endTime = new MongoDB\BSON\UTCDateTime($baseTime + 3600000); // 1小时范围
$rangeData = $bucketPattern->queryTimeRange($sensorId, $startTime, $endTime, 'hour');
print_r($rangeData);
?>

属性模式原理

属性模式使用对象数组存储可变属性,适合动态字段:

php
<?php
// 属性模式示例 - 产品可变属性存储
class AttributePattern {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function createProduct($productData, $attributes) {
        $collection = $this->database->selectCollection('products_attributes');
        
        $productDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'sku' => $productData['sku'],
            'name' => $productData['name'],
            'category' => $productData['category'],
            'price' => $productData['price'],
            'stock' => $productData['stock'],
            'attributes' => $this->formatAttributes($attributes),
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($productDocument);
        
        return $result->getInsertedId();
    }
    
    public function addProductAttribute($productId, $attributeName, $attributeValue, $attributeType = 'string') {
        $collection = $this->database->selectCollection('products_attributes');
        
        $attribute = [
            'name' => $attributeName,
            'value' => $attributeValue,
            'type' => $attributeType,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->updateOne(
            ['_id' => new MongoDB\BSON\ObjectId($productId)],
            [
                '$push' => ['attributes' => $attribute],
                '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
            ]
        );
        
        return [
            'modified_count' => $result->getModifiedCount(),
            'attribute_added' => $result->getModifiedCount() > 0
        ];
    }
    
    public function updateProductAttribute($productId, $attributeName, $newValue) {
        $collection = $this->database->selectCollection('products_attributes');
        
        $result = $collection->updateOne(
            [
                '_id' => new MongoDB\BSON\ObjectId($productId),
                'attributes.name' => $attributeName
            ],
            [
                '$set' => [
                    'attributes.$.value' => $newValue,
                    'attributes.$.updated_at' => new MongoDB\BSON\UTCDateTime(),
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ]
        );
        
        return [
            'modified_count' => $result->getModifiedCount(),
            'attribute_updated' => $result->getModifiedCount() > 0
        ];
    }
    
    public function searchProductsByAttribute($attributeName, $attributeValue) {
        $collection = $this->database->selectCollection('products_attributes');
        
        $products = $collection->find([
            'attributes' => [
                '$elemMatch' => [
                    'name' => $attributeName,
                    'value' => $attributeValue
                ]
            ]
        ])->toArray();
        
        return array_map(function($product) {
            return [
                'product_id' => $product['_id'],
                'sku' => $product['sku'],
                'name' => $product['name'],
                'category' => $product['category'],
                'price' => $product['price'],
                'matching_attributes' => array_filter($product['attributes'], function($attr) use ($attributeName, $attributeValue) {
                    return $attr['name'] === $attributeName && $attr['value'] === $attributeValue;
                })
            ];
        }, $products);
    }
    
    public function getProductWithAttributes($productId) {
        $collection = $this->database->selectCollection('products_attributes');
        
        $product = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($productId)]);
        
        if (!$product) {
            return null;
        }
        
        // 将属性数组转换为键值对对象
        $attributesObject = [];
        foreach ($product['attributes'] as $attribute) {
            $attributesObject[$attribute['name']] = [
                'value' => $attribute['value'],
                'type' => $attribute['type']
            ];
        }
        
        return [
            'product_id' => $product['_id'],
            'sku' => $product['sku'],
            'name' => $product['name'],
            'category' => $product['category'],
            'price' => $product['price'],
            'stock' => $product['stock'],
            'attributes' => $attributesObject,
            'attributes_count' => count($product['attributes'])
        ];
    }
    
    public function analyzeAttributePattern($productId) {
        $collection = $this->database->selectCollection('products_attributes');
        
        $product = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($productId)]);
        
        if (!$product) {
            return ['error' => 'Product not found'];
        }
        
        $documentSize = strlen(json_encode($product));
        $attributesCount = count($product['attributes']);
        
        // 分析属性类型分布
        $attributeTypes = [];
        foreach ($product['attributes'] as $attribute) {
            $type = $attribute['type'];
            if (!isset($attributeTypes[$type])) {
                $attributeTypes[$type] = 0;
            }
            $attributeTypes[$type]++;
        }
        
        return [
            'document_size' => $documentSize,
            'attributes_count' => $attributesCount,
            'avg_attribute_size' => $attributesCount > 0 ? round($documentSize / $attributesCount, 2) : 0,
            'attribute_types' => $attributeTypes,
            'flexibility_score' => min(100, $attributesCount * 10),
            'recommendation' => $attributesCount > 20 ? 
                'Consider using separate collection for attributes' : 
                'Attribute pattern is appropriate'
        ];
    }
    
    private function formatAttributes($attributes) {
        $formatted = [];
        foreach ($attributes as $name => $value) {
            $type = $this->getAttributeType($value);
            $formatted[] = [
                'name' => $name,
                'value' => $value,
                'type' => $type,
                'created_at' => new MongoDB\BSON\UTCDateTime()
            ];
        }
        return $formatted;
    }
    
    private function getAttributeType($value) {
        if (is_int($value)) {
            return 'integer';
        } elseif (is_float($value)) {
            return 'float';
        } elseif (is_bool($value)) {
            return 'boolean';
        } elseif (is_array($value)) {
            return 'array';
        } else {
            return 'string';
        }
    }
}

// 使用示例
$attributePattern = new AttributePattern('testdb');

// 创建产品及其属性
$productData = [
    'sku' => 'PRD001',
    'name' => 'Laptop Computer',
    'category' => 'Electronics',
    'price' => 999.99,
    'stock' => 50
];

$attributes = [
    'brand' => 'TechBrand',
    'model' => 'X1000',
    'processor' => 'Intel i7',
    'ram' => '16GB',
    'storage' => '512GB SSD',
    'screen_size' => '15.6 inch',
    'weight' => '2.5 kg',
    'color' => 'Silver',
    'warranty' => '2 years'
];

$productId = $attributePattern->createProduct($productData, $attributes);
echo "Created product with ID: " . $productId . "\n";

// 添加新属性
$attributePattern->addProductAttribute($productId, 'operating_system', 'Windows 11', 'string');
$attributePattern->addProductAttribute($productId, 'battery_life', '8 hours', 'string');

// 更新属性
$attributePattern->updateProductAttribute($productId, 'price', 899.99);

// 获取产品及其属性
$productWithAttributes = $attributePattern->getProductWithAttributes($productId);
print_r($productWithAttributes);

// 按属性搜索产品
$searchResults = $attributePattern->searchProductsByAttribute('brand', 'TechBrand');
print_r($searchResults);

// 分析属性模式
$analysis = $attributePattern->analyzeAttributePattern($productId);
print_r($analysis);
?>

常见错误与踩坑点

错误1:过度嵌入导致文档过大

问题描述:将过多数据嵌入到单个文档中,导致文档大小超过16MB限制。

php
<?php
// 错误示例 - 过度嵌入
try {
    $client = new MongoDB\Client("mongodb://localhost:27017");
    $database = $client->selectDatabase("testdb");
    $collection = $database->selectCollection('users_large');
    
    // 错误:嵌入过多订单数据
    $userDocument = [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'orders' => []
    ];
    
    // 添加1000个订单
    for ($i = 0; $i < 1000; $i++) {
        $userDocument['orders'][] = [
            'order_id' => 'ORD' . str_pad($i, 4, '0', STR_PAD_LEFT),
            'date' => new MongoDB\BSON\UTCDateTime(),
            'total' => rand(50, 500),
            'items' => [
                ['product_id' => 'P' . $i, 'name' => 'Product ' . $i, 'quantity' => rand(1, 5), 'price' => rand(10, 100)]
            ]
        ];
    }
    
    // 可能会失败,因为文档太大
    $result = $collection->insertOne($userDocument);
    
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

// 正确示例 - 使用引用模式
$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->selectDatabase("testdb");

// 创建用户文档
$userDocument = [
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'created_at' => new MongoDB\BSON\UTCDateTime()
];

$usersCollection = $database->selectCollection('users_normalized');
$userResult = $usersCollection->insertOne($userDocument);
$userId = $userResult->getInsertedId();

// 创建订单文档
$ordersCollection = $database->selectCollection('orders_normalized');
for ($i = 0; $i < 1000; $i++) {
    $orderDocument = [
        'user_id' => $userId,
        'order_id' => 'ORD' . str_pad($i, 4, '0', STR_PAD_LEFT),
        'date' => new MongoDB\BSON\UTCDateTime(),
        'total' => rand(50, 500),
        'items' => [
            ['product_id' => 'P' . $i, 'name' => 'Product ' . $i, 'quantity' => rand(1, 5), 'price' => rand(10, 100)]
        ]
    ];
    
    $ordersCollection->insertOne($orderDocument);
}

echo "Created user with ID: " . $userId . "\n";
echo "Created 1000 orders for the user\n";

// 查询用户及其订单
$user = $usersCollection->findOne(['_id' => $userId]);
$orders = $ordersCollection->find(['user_id' => $userId])->toArray();

echo "User: " . $user['name'] . "\n";
echo "Total orders: " . count($orders) . "\n";
?>

错误2:数组无限增长

问题描述:数组字段不断增长,导致文档大小超出限制。

php
<?php
// 错误示例 - 数组无限增长
try {
    $client = new MongoDB\Client("mongodb://localhost:27017");
    $database = $client->selectDatabase("testdb");
    $collection = $database->selectCollection('events_unbounded');
    
    // 错误:事件数组无限增长
    $eventDocument = [
        'user_id' => 'user_001',
        'events' => []
    ];
    
    $collection->insertOne($eventDocument);
    
    // 持续添加事件
    for ($i = 0; $i < 10000; $i++) {
        $collection->updateOne(
            ['user_id' => 'user_001'],
            ['$push' => ['events' => [
                'timestamp' => new MongoDB\BSON\UTCDateTime(),
                'event_type' => 'action_' . $i,
                'data' => ['value' => $i]
            ]]]
        );
    }
    
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

// 正确示例 - 使用固定集合或分桶
$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->selectDatabase("testdb");

// 方案1:使用固定集合
$database->createCollection('events_capped', [
    'capped' => true,
    'size' => 1024 * 1024 * 100, // 100MB
    'max' => 10000
]);

$eventsCappedCollection = $database->selectCollection('events_capped');

for ($i = 0; $i < 15000; $i++) {
    $eventsCappedCollection->insertOne([
        'user_id' => 'user_001',
        'timestamp' => new MongoDB\BSON\UTCDateTime(),
        'event_type' => 'action_' . $i,
        'data' => ['value' => $i]
    ]);
}

echo "Inserted 15000 events, but capped collection keeps only 10000\n";

// 方案2:使用分桶模式
$eventsBucketCollection = $database->selectCollection('events_buckets');

for ($i = 0; $i < 15000; $i++) {
    $timestamp = new MongoDB\BSON\UTCDateTime();
    $bucketKey = date('YmdH', $timestamp->toDateTime()->getTimestamp());
    
    $eventsBucketCollection->updateOne(
        [
            'user_id' => 'user_001',
            'bucket_key' => $bucketKey
        ],
        [
            '$push' => ['events' => [
                'timestamp' => $timestamp,
                'event_type' => 'action_' . $i,
                'data' => ['value' => $i]
            ]],
            '$inc' => ['count' => 1],
            '$setOnInsert' => [
                'created_at' => $timestamp
            ]
        ],
        ['upsert' => true]
    );
}

echo "Inserted 15000 events using bucket pattern\n";
?>

错误3:频繁更新导致文档移动

问题描述:频繁更新文档导致文档大小增长,触发文档移动。

php
<?php
// 错误示例 - 频繁更新导致文档移动
try {
    $client = new MongoDB\Client("mongodb://localhost:27017");
    $database = $client->selectDatabase("testdb");
    $collection = $database->selectCollection('products_frequent_updates');
    
    // 创建产品文档
    $productDocument = [
        'name' => 'Product A',
        'description' => 'Initial description',
        'price' => 99.99,
        'reviews' => []
    ];
    
    $result = $collection->insertOne($productDocument);
    $productId = $result->getInsertedId();
    
    // 错误:频繁添加评论,导致文档增长和移动
    for ($i = 0; $i < 1000; $i++) {
        $collection->updateOne(
            ['_id' => $productId],
            ['$push' => ['reviews' => [
                'user' => 'user_' . $i,
                'rating' => rand(1, 5),
                'comment' => 'Review comment ' . $i,
                'timestamp' => new MongoDB\BSON\UTCDateTime()
            ]]]
        );
    }
    
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

// 正确示例 - 使用引用模式
$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->selectDatabase("testdb");

// 创建产品文档
$productsCollection = $database->selectCollection('products_optimized');
$productDocument = [
    'name' => 'Product A',
    'description' => 'Initial description',
    'price' => 99.99,
    'review_count' => 0,
    'avg_rating' => 0
];

$productResult = $productsCollection->insertOne($productDocument);
$productId = $productResult->getInsertedId();

// 创建评论集合
$reviewsCollection = $database->selectCollection('reviews_optimized');

// 添加评论
for ($i = 0; $i < 1000; $i++) {
    $reviewDocument = [
        'product_id' => $productId,
        'user' => 'user_' . $i,
        'rating' => rand(1, 5),
        'comment' => 'Review comment ' . $i,
        'timestamp' => new MongoDB\BSON\UTCDateTime()
    ];
    
    $reviewsCollection->insertOne($reviewDocument);
}

// 更新产品统计信息
$totalReviews = $reviewsCollection->countDocuments(['product_id' => $productId]);
$avgRatingPipeline = [
    ['$match' => ['product_id' => $productId]],
    ['$group' => ['_id' => null, 'avg_rating' => ['$avg' => '$rating']]]
];
$avgRatingResult = iterator_to_array($reviewsCollection->aggregate($avgRatingPipeline))[0];

$productsCollection->updateOne(
    ['_id' => $productId],
    ['$set' => [
        'review_count' => $totalReviews,
        'avg_rating' => $avgRatingResult['avg_rating'] ?? 0
    ]]
);

echo "Created product with ID: " . $productId . "\n";
echo "Added 1000 reviews\n";
echo "Product review count: " . $totalReviews . "\n";
echo "Product average rating: " . round($avgRatingResult['avg_rating'] ?? 0, 2) . "\n";
?>

错误4:深度嵌套影响查询性能

问题描述:文档嵌套层次过深,影响查询性能和可读性。

php
<?php
// 错误示例 - 深度嵌套
try {
    $client = new MongoDB\Client("mongodb://localhost:27017");
    $database = $client->selectDatabase("testdb");
    $collection = $database->selectCollection('config_deep_nested');
    
    // 错误:深度嵌套的配置文档
    $configDocument = [
        'application' => [
            'name' => 'MyApp',
            'version' => '1.0.0',
            'settings' => [
                'database' => [
                    'connection' => [
                        'host' => 'localhost',
                        'port' => 27017,
                        'credentials' => [
                            'username' => 'admin',
                            'password' => 'password',
                            'options' => [
                                'ssl' => true,
                                'timeout' => [
                                    'connect' => 5000,
                                    'read' => 10000,
                                    'write' => 10000
                                ]
                            ]
                        ]
                    ]
                ],
                'cache' => [
                    'redis' => [
                        'host' => 'localhost',
                        'port' => 6379,
                        'options' => [
                            'ttl' => 3600,
                            'max_connections' => 100
                        ]
                    ]
                ]
            ]
        ]
    ];
    
    $collection->insertOne($configDocument);
    
    // 查询深层嵌套的数据很困难
    $result = $collection->findOne([
        'application.settings.database.connection.credentials.username' => 'admin'
    ]);
    
    echo "Found config: " . $result['application']['name'] . "\n";
    
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

// 正确示例 - 扁平化文档结构
$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->selectDatabase("testdb");

$collection = $database->selectCollection('config_flattened');

// 扁平化的配置文档
$configDocument = [
    'app_name' => 'MyApp',
    'app_version' => '1.0.0',
    'db_host' => 'localhost',
    'db_port' => 27017,
    'db_username' => 'admin',
    'db_password' => 'password',
    'db_ssl' => true,
    'db_connect_timeout' => 5000,
    'db_read_timeout' => 10000,
    'db_write_timeout' => 10000,
    'cache_host' => 'localhost',
    'cache_port' => 6379,
    'cache_ttl' => 3600,
    'cache_max_connections' => 100
];

$collection->insertOne($configDocument);

// 查询变得简单
$result = $collection->findOne(['db_username' => 'admin']);
echo "Found config: " . $result['app_name'] . "\n";

// 或者使用部分嵌套,保持逻辑分组
$collection = $database->selectCollection('config_optimized');

$configDocument = [
    'app_name' => 'MyApp',
    'app_version' => '1.0.0',
    'database' => [
        'host' => 'localhost',
        'port' => 27017,
        'username' => 'admin',
        'password' => 'password',
        'ssl' => true,
        'timeouts' => [
            'connect' => 5000,
            'read' => 10000,
            'write' => 10000
        ]
    ],
    'cache' => [
        'host' => 'localhost',
        'port' => 6379,
        'ttl' => 3600,
        'max_connections' => 100
    ]
];

$collection->insertOne($configDocument);

// 查询仍然相对简单
$result = $collection->findOne(['database.username' => 'admin']);
echo "Found config: " . $result['app_name'] . "\n";
?>

常见应用场景

场景1:电商系统商品目录

使用混合模式设计电商商品目录:

php
<?php
// 电商系统商品目录设计
class EcommerceProductCatalog {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function createProduct($productData) {
        $collection = $this->database->selectCollection('products');
        
        $productDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'sku' => $productData['sku'],
            'name' => $productData['name'],
            'description' => $productData['description'],
            'category' => [
                'main' => $productData['category']['main'],
                'sub' => $productData['category']['sub'] ?? null,
                'tags' => $productData['category']['tags'] ?? []
            ],
            'pricing' => [
                'regular' => $productData['pricing']['regular'],
                'sale' => $productData['pricing']['sale'] ?? null,
                'currency' => $productData['pricing']['currency'] ?? 'USD'
            ],
            'inventory' => [
                'quantity' => $productData['inventory']['quantity'],
                'reserved' => 0,
                'available' => $productData['inventory']['quantity'],
                'location' => $productData['inventory']['location'] ?? null
            ],
            'specifications' => $productData['specifications'] ?? [],
            'images' => $productData['images'] ?? [],
            'reviews' => [
                'count' => 0,
                'average_rating' => 0,
                'distribution' => [0, 0, 0, 0, 0] // 1-5 stars
            ],
            'seo' => [
                'title' => $productData['seo']['title'] ?? $productData['name'],
                'description' => $productData['seo']['description'] ?? $productData['description'],
                'keywords' => $productData['seo']['keywords'] ?? []
            ],
            'status' => $productData['status'] ?? 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($productDocument);
        
        return $result->getInsertedId();
    }
    
    public function createProductVariant($productId, $variantData) {
        $collection = $this->database->selectCollection('product_variants');
        
        $variantDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'product_id' => $productId,
            'sku' => $variantData['sku'],
            'name' => $variantData['name'],
            'attributes' => $variantData['attributes'], // e.g., ['color' => 'red', 'size' => 'M']
            'pricing' => [
                'regular' => $variantData['pricing']['regular'],
                'sale' => $variantData['pricing']['sale'] ?? null
            ],
            'inventory' => [
                'quantity' => $variantData['inventory']['quantity'],
                'reserved' => 0,
                'available' => $variantData['inventory']['quantity']
            ],
            'images' => $variantData['images'] ?? [],
            'status' => $variantData['status'] ?? 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($variantDocument);
        
        return $result->getInsertedId();
    }
    
    public function searchProducts($filters = [], $options = []) {
        $collection = $this->database->selectCollection('products');
        
        $query = $this->buildSearchQuery($filters);
        
        $defaultOptions = [
            'limit' => 20,
            'sort' => ['created_at' => -1]
        ];
        
        $searchOptions = array_merge($defaultOptions, $options);
        
        $cursor = $collection->find($query, $searchOptions);
        
        return iterator_to_array($cursor);
    }
    
    public function getProductWithVariants($productId) {
        $productsCollection = $this->database->selectCollection('products');
        $variantsCollection = $this->database->selectCollection('product_variants');
        
        $product = $productsCollection->findOne(['_id' => new MongoDB\BSON\ObjectId($productId)]);
        
        if (!$product) {
            return null;
        }
        
        $variants = $variantsCollection->find(['product_id' => new MongoDB\BSON\ObjectId($productId)])->toArray();
        
        return [
            'product' => $product,
            'variants' => $variants,
            'total_variants' => count($variants)
        ];
    }
    
    public function updateInventory($productId, $quantityChange, $variantId = null) {
        if ($variantId) {
            $collection = $this->database->selectCollection('product_variants');
            $filter = ['_id' => new MongoDB\BSON\ObjectId($variantId)];
        } else {
            $collection = $this->database->selectCollection('products');
            $filter = ['_id' => new MongoDB\BSON\ObjectId($productId)];
        }
        
        $result = $collection->updateOne(
            $filter,
            [
                '$inc' => [
                    'inventory.quantity' => $quantityChange,
                    'inventory.available' => $quantityChange
                ],
                '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
            ]
        );
        
        return [
            'modified_count' => $result->getModifiedCount(),
            'success' => $result->getModifiedCount() > 0
        ];
    }
    
    private function buildSearchQuery($filters) {
        $query = [];
        
        if (isset($filters['category'])) {
            $query['category.main'] = $filters['category'];
        }
        
        if (isset($filters['price_min'])) {
            $query['pricing.regular']['$gte'] = $filters['price_min'];
        }
        
        if (isset($filters['price_max'])) {
            $query['pricing.regular']['$lte'] = $filters['price_max'];
        }
        
        if (isset($filters['in_stock_only']) && $filters['in_stock_only']) {
            $query['inventory.available']['$gt'] = 0;
        }
        
        if (isset($filters['status'])) {
            $query['status'] = $filters['status'];
        }
        
        if (isset($filters['search'])) {
            $query['$or'] = [
                ['name' => new MongoDB\BSON\Regex($filters['search'], 'i')],
                ['description' => new MongoDB\BSON\Regex($filters['search'], 'i')],
                ['sku' => new MongoDB\BSON\Regex($filters['search'], 'i')]
            ];
        }
        
        return $query;
    }
}

// 使用示例
$catalog = new EcommerceProductCatalog('ecommerce_db');

// 创建产品
$productData = [
    'sku' => 'LAPTOP-001',
    'name' => 'High-Performance Laptop',
    'description' => 'A powerful laptop for professionals',
    'category' => [
        'main' => 'Electronics',
        'sub' => 'Computers',
        'tags' => ['laptop', 'computer', 'professional']
    ],
    'pricing' => [
        'regular' => 1299.99,
        'sale' => 1099.99,
        'currency' => 'USD'
    ],
    'inventory' => [
        'quantity' => 50,
        'location' => 'Warehouse A'
    ],
    'specifications' => [
        'processor' => 'Intel i7',
        'ram' => '16GB',
        'storage' => '512GB SSD',
        'display' => '15.6 inch'
    ],
    'images' => [
        'https://example.com/images/laptop1.jpg',
        'https://example.com/images/laptop2.jpg'
    ],
    'seo' => [
        'title' => 'High-Performance Laptop - Best for Professionals',
        'description' => 'Buy the best high-performance laptop for professionals',
        'keywords' => ['laptop', 'computer', 'professional', 'high-performance']
    ],
    'status' => 'active'
];

$productId = $catalog->createProduct($productData);
echo "Created product with ID: " . $productId . "\n";

// 创建产品变体
$variantData = [
    'sku' => 'LAPTOP-001-BLK',
    'name' => 'High-Performance Laptop - Black',
    'attributes' => [
        'color' => 'black',
        'warranty' => '2 years'
    ],
    'pricing' => [
        'regular' => 1299.99,
        'sale' => 1099.99
    ],
    'inventory' => [
        'quantity' => 25
    ],
    'images' => [
        'https://example.com/images/laptop-black.jpg'
    ]
];

$variantId = $catalog->createProductVariant($productId, $variantData);
echo "Created variant with ID: " . $variantId . "\n";

// 搜索产品
$searchResults = $catalog->searchProducts([
    'category' => 'Electronics',
    'price_min' => 500,
    'price_max' => 2000,
    'in_stock_only' => true
]);
echo "Found " . count($searchResults) . " products\n";

// 获取产品及其变体
$productWithVariants = $catalog->getProductWithVariants($productId);
print_r($productWithVariants);

// 更新库存
$updateResult = $catalog->updateInventory($productId, -1, $variantId);
print_r($updateResult);
?>

场景2:社交媒体内容管理

使用嵌入模式设计社交媒体内容:

php
<?php
// 社交媒体内容管理系统
class SocialMediaContentManager {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function createPost($postData) {
        $collection = $this->database->selectCollection('posts');
        
        $postDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'author_id' => $postData['author_id'],
            'author_name' => $postData['author_name'],
            'author_avatar' => $postData['author_avatar'] ?? null,
            'content' => [
                'text' => $postData['content']['text'] ?? '',
                'images' => $postData['content']['images'] ?? [],
                'videos' => $postData['content']['videos'] ?? [],
                'links' => $postData['content']['links'] ?? []
            ],
            'metadata' => [
                'type' => $postData['metadata']['type'] ?? 'post',
                'visibility' => $postData['metadata']['visibility'] ?? 'public',
                'location' => $postData['metadata']['location'] ?? null,
                'tags' => $postData['metadata']['tags'] ?? [],
                'mentions' => $postData['metadata']['mentions'] ?? []
            ],
            'interactions' => [
                'likes' => [],
                'comments' => [],
                'shares' => 0,
                'views' => 0
            ],
            'statistics' => [
                'like_count' => 0,
                'comment_count' => 0,
                'share_count' => 0,
                'view_count' => 0
            ],
            'status' => 'published',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($postDocument);
        
        return $result->getInsertedId();
    }
    
    public function addComment($postId, $commentData) {
        $collection = $this->database->selectCollection('posts');
        
        $comment = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'author_id' => $commentData['author_id'],
            'author_name' => $commentData['author_name'],
            'author_avatar' => $commentData['author_avatar'] ?? null,
            'text' => $commentData['text'],
            'parent_id' => $commentData['parent_id'] ?? null,
            'likes' => [],
            'like_count' => 0,
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->updateOne(
            ['_id' => new MongoDB\BSON\ObjectId($postId)],
            [
                '$push' => ['interactions.comments' => $comment],
                '$inc' => ['statistics.comment_count' => 1],
                '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
            ]
        );
        
        return [
            'comment_id' => $comment['_id'],
            'success' => $result->getModifiedCount() > 0
        ];
    }
    
    public function likePost($postId, $userId) {
        $collection = $this->database->selectCollection('posts');
        
        // 检查是否已经点赞
        $post = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($postId)]);
        $alreadyLiked = in_array($userId, $post['interactions']['likes']);
        
        if ($alreadyLiked) {
            // 取消点赞
            $result = $collection->updateOne(
                ['_id' => new MongoDB\BSON\ObjectId($postId)],
                [
                    '$pull' => ['interactions.likes' => $userId],
                    '$inc' => ['statistics.like_count' => -1],
                    '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
                ]
            );
            
            return [
                'action' => 'unliked',
                'success' => $result->getModifiedCount() > 0
            ];
        } else {
            // 点赞
            $result = $collection->updateOne(
                ['_id' => new MongoDB\BSON\ObjectId($postId)],
                [
                    '$push' => ['interactions.likes' => $userId],
                    '$inc' => ['statistics.like_count' => 1],
                    '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
                ]
            );
            
            return [
                'action' => 'liked',
                'success' => $result->getModifiedCount() > 0
            ];
        }
    }
    
    public function getPostWithComments($postId, $commentLimit = 50) {
        $collection = $this->database->selectCollection('posts');
        
        $post = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($postId)]);
        
        if (!$post) {
            return null;
        }
        
        // 限制评论数量
        $post['interactions']['comments'] = array_slice(
            $post['interactions']['comments'],
            0,
            $commentLimit
        );
        
        return $post;
    }
    
    public function getUserFeed($userId, $options = []) {
        $collection = $this->database->selectCollection('posts');
        
        $defaultOptions = [
            'limit' => 20,
            'skip' => 0,
            'sort' => ['created_at' => -1]
        ];
        
        $feedOptions = array_merge($defaultOptions, $options);
        
        // 这里简化处理,实际应该根据用户关注关系获取动态
        $cursor = $collection->find(
            ['status' => 'published'],
            $feedOptions
        );
        
        return iterator_to_array($cursor);
    }
    
    public function searchPosts($searchTerm, $options = []) {
        $collection = $this->database->selectCollection('posts');
        
        $defaultOptions = [
            'limit' => 20,
            'skip' => 0,
            'sort' => ['created_at' => -1]
        ];
        
        $searchOptions = array_merge($defaultOptions, $options);
        
        $query = [
            '$or' => [
                ['content.text' => new MongoDB\BSON\Regex($searchTerm, 'i')],
                ['metadata.tags' => new MongoDB\BSON\Regex($searchTerm, 'i')],
                ['author_name' => new MongoDB\BSON\Regex($searchTerm, 'i')]
            ],
            'status' => 'published'
        ];
        
        $cursor = $collection->find($query, $searchOptions);
        
        return iterator_to_array($cursor);
    }
}

// 使用示例
$socialManager = new SocialMediaContentManager('social_media_db');

// 创建帖子
$postData = [
    'author_id' => 'user_001',
    'author_name' => 'John Doe',
    'author_avatar' => 'https://example.com/avatars/john.jpg',
    'content' => [
        'text' => 'Just arrived in New York! The city is amazing 🗽',
        'images' => [
            'https://example.com/images/nyc1.jpg',
            'https://example.com/images/nyc2.jpg'
        ]
    ],
    'metadata' => [
        'type' => 'post',
        'visibility' => 'public',
        'location' => 'New York, USA',
        'tags' => ['travel', 'new york', 'vacation'],
        'mentions' => ['@jane_smith']
    ]
];

$postId = $socialManager->createPost($postData);
echo "Created post with ID: " . $postId . "\n";

// 添加评论
$commentData = [
    'author_id' => 'user_002',
    'author_name' => 'Jane Smith',
    'author_avatar' => 'https://example.com/avatars/jane.jpg',
    'text' => 'Have a great time in NYC! 🎉'
];

$commentResult = $socialManager->addComment($postId, $commentData);
print_r($commentResult);

// 点赞帖子
$likeResult = $socialManager->likePost($postId, 'user_003');
print_r($likeResult);

// 获取帖子及其评论
$postWithComments = $socialManager->getPostWithComments($postId);
print_r($postWithComments);

// 搜索帖子
$searchResults = $socialManager->searchPosts('New York');
echo "Found " . count($searchResults) . " posts\n";
?>

场景3:物联网设备数据采集

使用桶模式设计物联网数据采集系统:

php
<?php
// 物联网设备数据采集系统
class IoTDataCollector {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function registerDevice($deviceData) {
        $collection = $this->database->selectCollection('devices');
        
        $deviceDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'device_id' => $deviceData['device_id'],
            'name' => $deviceData['name'],
            'type' => $deviceData['type'],
            'location' => $deviceData['location'],
            'specifications' => $deviceData['specifications'] ?? [],
            'status' => 'active',
            'last_seen' => null,
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($deviceDocument);
        
        return $result->getInsertedId();
    }
    
    public function collectData($deviceId, $timestamp, $readings) {
        $collection = $this->database->selectCollection('device_data');
        
        $bucketKey = $this->generateBucketKey($timestamp, 'hour');
        $bucketId = $deviceId . '_' . $bucketKey;
        
        $reading = [
            'timestamp' => $timestamp,
            'readings' => $readings
        ];
        
        $result = $collection->updateOne(
            ['_id' => $bucketId],
            [
                '$push' => ['data_points' => $reading],
                '$inc' => ['count' => 1],
                '$set' => [
                    'last_timestamp' => $timestamp,
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ],
                '$setOnInsert' => [
                    'device_id' => $deviceId,
                    'bucket_key' => $bucketKey,
                    'start_time' => $this->getBucketStartTime($timestamp, 'hour'),
                    'end_time' => $this->getBucketEndTime($timestamp, 'hour'),
                    'created_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ],
            ['upsert' => true]
        );
        
        // 更新设备最后活跃时间
        $this->updateDeviceLastSeen($deviceId, $timestamp);
        
        return [
            'bucket_id' => $bucketId,
            'success' => $result->getModifiedCount() > 0 || $result->getUpsertedCount() > 0
        ];
    }
    
    public function getDeviceData($deviceId, $startTime, $endTime, $bucketSize = 'hour') {
        $collection = $this->database->selectCollection('device_data');
        
        $startBucketKey = $this->generateBucketKey($startTime, $bucketSize);
        $endBucketKey = $this->generateBucketKey($endTime, $bucketSize);
        
        $buckets = $collection->find([
            'device_id' => $deviceId,
            'bucket_key' => ['$gte' => $startBucketKey, '$lte' => $endBucketKey]
        ])->toArray();
        
        $allDataPoints = [];
        
        foreach ($buckets as $bucket) {
            $filteredDataPoints = array_filter($bucket['data_points'], function($dataPoint) use ($startTime, $endTime) {
                $timestamp = $dataPoint['timestamp'];
                return $timestamp >= $startTime && $timestamp <= $endTime;
            });
            
            $allDataPoints = array_merge($allDataPoints, $filteredDataPoints);
        }
        
        // 按时间排序
        usort($allDataPoints, function($a, $b) {
            return $a['timestamp']->toDateTime() <=> $b['timestamp']->toDateTime();
        });
        
        return [
            'device_id' => $deviceId,
            'time_range' => [
                'start' => $startTime,
                'end' => $endTime
            ],
            'data_points' => $allDataPoints,
            'total_points' => count($allDataPoints)
        ];
    }
    
    public function getDeviceStatistics($deviceId, $startTime, $endTime) {
        $collection = $this->database->selectCollection('device_data');
        
        $startBucketKey = $this->generateBucketKey($startTime, 'hour');
        $endBucketKey = $this->generateBucketKey($endTime, 'hour');
        
        $buckets = $collection->find([
            'device_id' => $deviceId,
            'bucket_key' => ['$gte' => $startBucketKey, '$lte' => $endBucketKey]
        ])->toArray();
        
        $statistics = [
            'total_points' => 0,
            'readings' => []
        ];
        
        foreach ($buckets as $bucket) {
            $statistics['total_points'] += $bucket['count'];
            
            foreach ($bucket['data_points'] as $dataPoint) {
                foreach ($dataPoint['readings'] as $key => $value) {
                    if (!isset($statistics['readings'][$key])) {
                        $statistics['readings'][$key] = [
                            'count' => 0,
                            'sum' => 0,
                            'min' => null,
                            'max' => null
                        ];
                    }
                    
                    $statistics['readings'][$key]['count']++;
                    $statistics['readings'][$key]['sum'] += $value;
                    
                    if ($statistics['readings'][$key]['min'] === null || $value < $statistics['readings'][$key]['min']) {
                        $statistics['readings'][$key]['min'] = $value;
                    }
                    
                    if ($statistics['readings'][$key]['max'] === null || $value > $statistics['readings'][$key]['max']) {
                        $statistics['readings'][$key]['max'] = $value;
                    }
                }
            }
        }
        
        // 计算平均值
        foreach ($statistics['readings'] as $key => $stats) {
            if ($stats['count'] > 0) {
                $statistics['readings'][$key]['avg'] = $stats['sum'] / $stats['count'];
            }
        }
        
        return $statistics;
    }
    
    private function updateDeviceLastSeen($deviceId, $timestamp) {
        $collection = $this->database->selectCollection('devices');
        
        $collection->updateOne(
            ['device_id' => $deviceId],
            [
                '$set' => [
                    'last_seen' => $timestamp,
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ]
        );
    }
    
    private function generateBucketKey($timestamp, $bucketSize) {
        $dateTime = $timestamp->toDateTime();
        
        switch ($bucketSize) {
            case 'minute':
                return $dateTime->format('YmdHi');
            case 'hour':
                return $dateTime->format('YmdH');
            case 'day':
                return $dateTime->format('Ymd');
            default:
                return $dateTime->format('YmdH');
        }
    }
    
    private function getBucketStartTime($timestamp, $bucketSize) {
        $dateTime = clone $timestamp->toDateTime();
        
        switch ($bucketSize) {
            case 'minute':
                $dateTime->setTime($dateTime->format('H'), $dateTime->format('i'), 0);
                break;
            case 'hour':
                $dateTime->setTime($dateTime->format('H'), 0, 0);
                break;
            case 'day':
                $dateTime->setTime(0, 0, 0);
                break;
        }
        
        return new MongoDB\BSON\UTCDateTime($dateTime->getTimestamp() * 1000);
    }
    
    private function getBucketEndTime($timestamp, $bucketSize) {
        $startTime = $this->getBucketStartTime($timestamp, $bucketSize);
        $dateTime = clone $startTime->toDateTime();
        
        switch ($bucketSize) {
            case 'minute':
                $dateTime->modify('+1 minute');
                break;
            case 'hour':
                $dateTime->modify('+1 hour');
                break;
            case 'day':
                $dateTime->modify('+1 day');
                break;
        }
        
        return new MongoDB\BSON\UTCDateTime($dateTime->getTimestamp() * 1000);
    }
}

// 使用示例
$iotCollector = new IoTDataCollector('iot_db');

// 注册设备
$deviceData = [
    'device_id' => 'sensor_temp_001',
    'name' => 'Temperature Sensor 1',
    'type' => 'temperature',
    'location' => 'Building A, Floor 1',
    'specifications' => [
        'range' => '-40 to 85°C',
        'accuracy' => '±0.5°C',
        'sampling_rate' => '1 minute'
    ]
];

$deviceId = $iotCollector->registerDevice($deviceData);
echo "Registered device with ID: " . $deviceId . "\n";

// 采集数据
$baseTime = time() * 1000;
for ($i = 0; $i < 120; $i++) {
    $timestamp = new MongoDB\BSON\UTCDateTime($baseTime + $i * 60000); // 每分钟
    $readings = [
        'temperature' => 20 + rand(-5, 5),
        'humidity' => 50 + rand(-10, 10),
        'pressure' => 1013 + rand(-5, 5)
    ];
    
    $result = $iotCollector->collectData('sensor_temp_001', $timestamp, $readings);
}

echo "Collected 120 data points\n";

// 查询设备数据
$startTime = new MongoDB\BSON\UTCDateTime($baseTime);
$endTime = new MongoDB\BSON\UTCDateTime($baseTime + 3600000); // 1小时范围
$deviceData = $iotCollector->getDeviceData('sensor_temp_001', $startTime, $endTime);
echo "Retrieved " . $deviceData['total_points'] . " data points\n";

// 获取设备统计信息
$statistics = $iotCollector->getDeviceStatistics('sensor_temp_001', $startTime, $endTime);
print_r($statistics);
?>

企业级进阶应用场景

场景1:多租户SaaS平台架构

使用文档隔离策略设计多租户SaaS平台:

php
<?php
// 多租户SaaS平台架构
class MultiTenantSaaSPlatform {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function createTenant($tenantData) {
        $collection = $this->database->selectCollection('tenants');
        
        $tenantDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'tenant_id' => $tenantData['tenant_id'],
            'name' => $tenantData['name'],
            'domain' => $tenantData['domain'],
            'plan' => $tenantData['plan'],
            'settings' => [
                'timezone' => $tenantData['settings']['timezone'] ?? 'UTC',
                'language' => $tenantData['settings']['language'] ?? 'en',
                'currency' => $tenantData['settings']['currency'] ?? 'USD',
                'features' => $tenantData['settings']['features'] ?? []
            ],
            'limits' => [
                'users' => $tenantData['limits']['users'] ?? 10,
                'storage' => $tenantData['limits']['storage'] ?? 1024 * 1024 * 1024, // 1GB
                'api_calls' => $tenantData['limits']['api_calls'] ?? 10000
            ],
            'usage' => [
                'users' => 0,
                'storage' => 0,
                'api_calls' => 0
            ],
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($tenantDocument);
        
        return $result->getInsertedId();
    }
    
    public function createTenantUser($tenantId, $userData) {
        $collection = $this->database->selectCollection('users');
        
        $userDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'tenant_id' => $tenantId,
            'user_id' => $userData['user_id'],
            'email' => $userData['email'],
            'name' => $userData['name'],
            'role' => $userData['role'] ?? 'user',
            'permissions' => $userData['permissions'] ?? [],
            'profile' => $userData['profile'] ?? [],
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime(),
            'last_login' => null
        ];
        
        $result = $collection->insertOne($userDocument);
        
        // 更新租户用户数量
        $this->updateTenantUsage($tenantId, 'users', 1);
        
        return $result->getInsertedId();
    }
    
    public function createTenantResource($tenantId, $resourceType, $resourceData) {
        $collectionName = $resourceType;
        $collection = $this->database->selectCollection($collectionName);
        
        $resourceDocument = array_merge($resourceData, [
            '_id' => new MongoDB\BSON\ObjectId(),
            'tenant_id' => $tenantId,
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        $result = $collection->insertOne($resourceDocument);
        
        // 更新租户存储使用量
        $documentSize = strlen(json_encode($resourceDocument));
        $this->updateTenantUsage($tenantId, 'storage', $documentSize);
        
        return $result->getInsertedId();
    }
    
    public function getTenantResources($tenantId, $resourceType, $filters = [], $options = []) {
        $collection = $this->database->selectCollection($resourceType);
        
        $query = array_merge($filters, ['tenant_id' => $tenantId]);
        
        $defaultOptions = [
            'limit' => 100,
            'sort' => ['created_at' => -1]
        ];
        
        $resourceOptions = array_merge($defaultOptions, $options);
        
        $cursor = $collection->find($query, $resourceOptions);
        
        return iterator_to_array($cursor);
    }
    
    public function getTenantStatistics($tenantId) {
        $tenantsCollection = $this->database->selectCollection('tenants');
        $usersCollection = $this->database->selectCollection('users');
        
        $tenant = $tenantsCollection->findOne(['tenant_id' => $tenantId]);
        
        if (!$tenant) {
            return null;
        }
        
        $userCount = $usersCollection->countDocuments(['tenant_id' => $tenantId]);
        
        return [
            'tenant_id' => $tenantId,
            'name' => $tenant['name'],
            'plan' => $tenant['plan'],
            'limits' => $tenant['limits'],
            'usage' => [
                'users' => $userCount,
                'storage' => $tenant['usage']['storage'],
                'api_calls' => $tenant['usage']['api_calls']
            ],
            'usage_percentage' => [
                'users' => round(($userCount / $tenant['limits']['users']) * 100, 2),
                'storage' => round(($tenant['usage']['storage'] / $tenant['limits']['storage']) * 100, 2),
                'api_calls' => round(($tenant['usage']['api_calls'] / $tenant['limits']['api_calls']) * 100, 2)
            ]
        ];
    }
    
    private function updateTenantUsage($tenantId, $usageType, $amount) {
        $collection = $this->database->selectCollection('tenants');
        
        $collection->updateOne(
            ['tenant_id' => $tenantId],
            [
                '$inc' => ['usage.' . $usageType => $amount],
                '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
            ]
        );
    }
}

// 使用示例
$saasPlatform = new MultiTenantSaaSPlatform('saas_db');

// 创建租户
$tenantData = [
    'tenant_id' => 'tenant_acme',
    'name' => 'ACME Corporation',
    'domain' => 'acme.saasplatform.com',
    'plan' => 'enterprise',
    'settings' => [
        'timezone' => 'America/New_York',
        'language' => 'en',
        'currency' => 'USD',
        'features' => ['advanced_analytics', 'api_access', 'custom_integrations']
    ],
    'limits' => [
        'users' => 100,
        'storage' => 1024 * 1024 * 1024 * 10, // 10GB
        'api_calls' => 100000
    ]
];

$tenantId = $saasPlatform->createTenant($tenantData);
echo "Created tenant with ID: " . $tenantId . "\n";

// 创建租户用户
$userData = [
    'user_id' => 'user_john',
    'email' => 'john@acme.com',
    'name' => 'John Doe',
    'role' => 'admin',
    'permissions' => ['read', 'write', 'delete', 'admin'],
    'profile' => [
        'department' => 'IT',
        'position' => 'System Administrator'
    ]
];

$userId = $saasPlatform->createTenantUser('tenant_acme', $userData);
echo "Created user with ID: " . $userId . "\n";

// 创建租户资源
$projectData = [
    'project_id' => 'proj_001',
    'name' => 'Website Redesign',
    'description' => 'Redesign company website',
    'status' => 'active',
    'budget' => 50000,
    'team' => ['user_john', 'user_jane']
];

$projectId = $saasPlatform->createTenantResource('tenant_acme', 'projects', $projectData);
echo "Created project with ID: " . $projectId . "\n";

// 获取租户资源
$projects = $saasPlatform->getTenantResources('tenant_acme', 'projects');
echo "Found " . count($projects) . " projects\n";

// 获取租户统计信息
$statistics = $saasPlatform->getTenantStatistics('tenant_acme');
print_r($statistics);
?>

行业最佳实践

最佳实践1:文档大小管理

php
<?php
// 文档大小管理最佳实践
class DocumentSizeManager {
    private $database;
    private $maxDocumentSize = 16 * 1024 * 1024; // 16MB
    private $warningThreshold = 10 * 1024 * 1024; // 10MB
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function checkDocumentSize($collectionName, $documentId) {
        $collection = $this->database->selectCollection($collectionName);
        
        $document = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($documentId)]);
        
        if (!$document) {
            return ['error' => 'Document not found'];
        }
        
        $documentSize = strlen(json_encode($document));
        $sizePercentage = ($documentSize / $this->maxDocumentSize) * 100;
        
        return [
            'document_id' => $documentId,
            'size_bytes' => $documentSize,
            'size_mb' => round($documentSize / (1024 * 1024), 2),
            'size_percentage' => round($sizePercentage, 2),
            'status' => $this->getSizeStatus($documentSize),
            'recommendation' => $this->getSizeRecommendation($documentSize)
        ];
    }
    
    public function optimizeLargeDocument($collectionName, $documentId) {
        $collection = $this->database->selectCollection($collectionName);
        
        $document = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($documentId)]);
        
        if (!$document) {
            return ['error' => 'Document not found'];
        }
        
        $optimizations = [];
        
        // 检查大数组
        foreach ($document as $key => $value) {
            if (is_array($value) && count($value) > 100) {
                $optimizations[] = [
                    'field' => $key,
                    'issue' => 'Large array',
                    'count' => count($value),
                    'suggestion' => 'Consider moving to separate collection'
                ];
            }
        }
        
        // 检查嵌套深度
        $maxDepth = $this->calculateMaxDepth($document);
        if ($maxDepth > 5) {
            $optimizations[] = [
                'field' => 'document',
                'issue' => 'Deep nesting',
                'depth' => $maxDepth,
                'suggestion' => 'Consider flattening document structure'
            ];
        }
        
        return [
            'document_id' => $documentId,
            'optimizations' => $optimizations,
            'total_issues' => count($optimizations)
        ];
    }
    
    private function getSizeStatus($size) {
        if ($size >= $this->maxDocumentSize) {
            return 'critical';
        } elseif ($size >= $this->warningThreshold) {
            return 'warning';
        } else {
            return 'normal';
        }
    }
    
    private function getSizeRecommendation($size) {
        if ($size >= $this->maxDocumentSize) {
            return 'Document exceeds maximum size. Must split immediately.';
        } elseif ($size >= $this->warningThreshold) {
            return 'Document is approaching size limit. Consider optimization.';
        } else {
            return 'Document size is acceptable.';
        }
    }
    
    private function calculateMaxDepth($data, $currentDepth = 1) {
        $maxDepth = $currentDepth;
        
        if (is_array($data)) {
            foreach ($data as $value) {
                if (is_array($value)) {
                    $depth = $this->calculateMaxDepth($value, $currentDepth + 1);
                    if ($depth > $maxDepth) {
                        $maxDepth = $depth;
                    }
                }
            }
        }
        
        return $maxDepth;
    }
}

// 使用示例
$sizeManager = new DocumentSizeManager('testdb');

// 检查文档大小
$sizeCheck = $sizeManager->checkDocumentSize('users', '507f1f77bcf86cd799439011');
print_r($sizeCheck);

// 优化大文档
$optimization = $sizeManager->optimizeLargeDocument('users', '507f1f77bcf86cd799439011');
print_r($optimization);
?>

常见问题答疑

问题1:如何选择嵌入还是引用?

回答:选择嵌入还是引用需要考虑多个因素:

php
<?php
// 嵌入 vs 引用决策助手
class EmbeddingVsReferenceDecision {
    public static function makeDecision($relationship, $accessPattern, $dataVolume, $updateFrequency) {
        $score = 0;
        $factors = [];
        
        // 访问模式评分
        if ($accessPattern === 'always_together') {
            $score += 3;
            $factors[] = ['factor' => 'access_pattern', 'points' => 3, 'reason' => 'Data accessed together'];
        } elseif ($accessPattern === 'separate_access') {
            $score -= 2;
            $factors[] = ['factor' => 'access_pattern', 'points' => -2, 'reason' => 'Data accessed separately'];
        }
        
        // 数据量评分
        if ($dataVolume === 'small') {
            $score += 2;
            $factors[] = ['factor' => 'data_volume', 'points' => 2, 'reason' => 'Small data volume'];
        } elseif ($dataVolume === 'large') {
            $score -= 3;
            $factors[] = ['factor' => 'data_volume', 'points' => -3, 'reason' => 'Large data volume'];
        }
        
        // 更新频率评分
        if ($updateFrequency === 'rare') {
            $score += 1;
            $factors[] = ['factor' => 'update_frequency', 'points' => 1, 'reason' => 'Rare updates'];
        } elseif ($updateFrequency === 'frequent') {
            $score -= 2;
            $factors[] = ['factor' => 'update_frequency', 'points' => -2, 'reason' => 'Frequent updates'];
        }
        
        // 关系类型评分
        if ($relationship === 'one_to_one' || $relationship === 'one_to_few') {
            $score += 2;
            $factors[] = ['factor' => 'relationship', 'points' => 2, 'reason' => 'One-to-one or one-to-few relationship'];
        } elseif ($relationship === 'one_to_many' || $relationship === 'many_to_many') {
            $score -= 1;
            $factors[] = ['factor' => 'relationship', 'points' => -1, 'reason' => 'One-to-many or many-to-many relationship'];
        }
        
        return [
            'recommendation' => $score > 0 ? 'embed' : 'reference',
            'score' => $score,
            'factors' => $factors
        ];
    }
}

// 使用示例
$decision = EmbeddingVsReferenceDecision::makeDecision(
    'one_to_many',
    'separate_access',
    'large',
    'frequent'
);
print_r($decision);
?>

实战练习

练习1:设计博客系统文档结构

设计一个博客系统的文档结构,包括用户、文章、评论等。

php
<?php
// 博客系统文档结构设计
class BlogSystemDesign {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function createUser($userData) {
        $collection = $this->database->selectCollection('users');
        
        $userDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'username' => $userData['username'],
            'email' => $userData['email'],
            'password_hash' => password_hash($userData['password'], PASSWORD_DEFAULT),
            'profile' => [
                'display_name' => $userData['profile']['display_name'] ?? $userData['username'],
                'bio' => $userData['profile']['bio'] ?? '',
                'avatar' => $userData['profile']['avatar'] ?? null,
                'website' => $userData['profile']['website'] ?? null
            ],
            'settings' => [
                'theme' => $userData['settings']['theme'] ?? 'light',
                'notifications' => $userData['settings']['notifications'] ?? true
            ],
            'statistics' => [
                'posts_count' => 0,
                'comments_count' => 0,
                'followers_count' => 0,
                'following_count' => 0
            ],
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($userDocument);
        
        return $result->getInsertedId();
    }
    
    public function createPost($userId, $postData) {
        $collection = $this->database->selectCollection('posts');
        
        $postDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'author_id' => $userId,
            'title' => $postData['title'],
            'slug' => $this->generateSlug($postData['title']),
            'content' => $postData['content'],
            'excerpt' => $postData['excerpt'] ?? substr($postData['content'], 0, 200),
            'featured_image' => $postData['featured_image'] ?? null,
            'category' => $postData['category'],
            'tags' => $postData['tags'] ?? [],
            'metadata' => [
                'status' => $postData['metadata']['status'] ?? 'draft',
                'visibility' => $postData['metadata']['visibility'] ?? 'public',
                'allow_comments' => $postData['metadata']['allow_comments'] ?? true
            ],
            'statistics' => [
                'views' => 0,
                'likes' => 0,
                'comments_count' => 0,
                'shares' => 0
            ],
            'seo' => [
                'meta_title' => $postData['seo']['meta_title'] ?? $postData['title'],
                'meta_description' => $postData['seo']['meta_description'] ?? $postData['excerpt'],
                'keywords' => $postData['seo']['keywords'] ?? []
            ],
            'published_at' => $postData['metadata']['status'] === 'published' ? new MongoDB\BSON\UTCDateTime() : null,
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($postDocument);
        
        // 更新用户文章统计
        $this->updateUserStatistics($userId, 'posts_count', 1);
        
        return $result->getInsertedId();
    }
    
    public function addComment($postId, $userId, $commentData) {
        $collection = $this->database->selectCollection('comments');
        
        $commentDocument = [
            '_id' => new MongoDB\BSON\ObjectId(),
            'post_id' => $postId,
            'author_id' => $userId,
            'parent_id' => $commentData['parent_id'] ?? null,
            'content' => $commentData['content'],
            'status' => 'approved',
            'likes' => [],
            'like_count' => 0,
            'created_at' => new MongoDB\BSON\UTCDateTime(),
            'updated_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($commentDocument);
        
        // 更新文章评论统计
        $this->updatePostStatistics($postId, 'comments_count', 1);
        
        // 更新用户评论统计
        $this->updateUserStatistics($userId, 'comments_count', 1);
        
        return $result->getInsertedId();
    }
    
    public function getPostWithComments($postId) {
        $postsCollection = $this->database->selectCollection('posts');
        $commentsCollection = $this->database->selectCollection('comments');
        $usersCollection = $this->database->selectCollection('users');
        
        $post = $postsCollection->findOne(['_id' => new MongoDB\BSON\ObjectId($postId)]);
        
        if (!$post) {
            return null;
        }
        
        // 获取评论
        $comments = $commentsCollection->find(['post_id' => new MongoDB\BSON\ObjectId($postId)])->toArray();
        
        // 获取作者信息
        $author = $usersCollection->findOne(['_id' => $post['author_id']]);
        
        return [
            'post' => $post,
            'author' => [
                'user_id' => $author['_id'],
                'username' => $author['username'],
                'display_name' => $author['profile']['display_name'],
                'avatar' => $author['profile']['avatar']
            ],
            'comments' => $comments,
            'total_comments' => count($comments)
        ];
    }
    
    private function generateSlug($title) {
        $slug = strtolower($title);
        $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
        $slug = trim($slug, '-');
        return $slug;
    }
    
    private function updateUserStatistics($userId, $field, $increment) {
        $collection = $this->database->selectCollection('users');
        
        $collection->updateOne(
            ['_id' => new MongoDB\BSON\ObjectId($userId)],
            [
                '$inc' => ['statistics.' . $field => $increment],
                '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
            ]
        );
    }
    
    private function updatePostStatistics($postId, $field, $increment) {
        $collection = $this->database->selectCollection('posts');
        
        $collection->updateOne(
            ['_id' => new MongoDB\BSON\ObjectId($postId)],
            [
                '$inc' => ['statistics.' . $field => $increment],
                '$set' => ['updated_at' => new MongoDB\BSON\UTCDateTime()]
            ]
        );
    }
}

// 使用示例
$blogSystem = new BlogSystemDesign('blog_db');

// 创建用户
$userData = [
    'username' => 'johndoe',
    'email' => 'john@example.com',
    'password' => 'securepassword123',
    'profile' => [
        'display_name' => 'John Doe',
        'bio' => 'Tech blogger and developer',
        'avatar' => 'https://example.com/avatars/john.jpg'
    ],
    'settings' => [
        'theme' => 'dark',
        'notifications' => true
    ]
];

$userId = $blogSystem->createUser($userData);
echo "Created user with ID: " . $userId . "\n";

// 创建文章
$postData = [
    'title' => 'Getting Started with MongoDB',
    'content' => 'MongoDB is a powerful NoSQL database...',
    'excerpt' => 'Learn the basics of MongoDB and how to get started...',
    'category' => 'Database',
    'tags' => ['mongodb', 'nosql', 'database', 'tutorial'],
    'metadata' => [
        'status' => 'published',
        'visibility' => 'public',
        'allow_comments' => true
    ],
    'seo' => [
        'meta_title' => 'Getting Started with MongoDB - Complete Guide',
        'meta_description' => 'Learn the basics of MongoDB and how to get started with this powerful NoSQL database.',
        'keywords' => ['mongodb', 'nosql', 'database', 'tutorial', 'getting started']
    ]
];

$postId = $blogSystem->createPost($userId, $postData);
echo "Created post with ID: " . $postId . "\n";

// 添加评论
$commentData = [
    'content' => 'Great article! Very helpful for beginners.'
];

$commentId = $blogSystem->addComment($postId, $userId, $commentData);
echo "Created comment with ID: " . $commentId . "\n";

// 获取文章及其评论
$postWithComments = $blogSystem->getPostWithComments($postId);
print_r($postWithComments);
?>

知识点总结

核心概念

  1. 文档结构:MongoDB使用BSON格式存储文档,支持嵌套和数组
  2. 嵌入模式:将相关数据存储在同一文档中,适合一对少关系
  3. 引用模式:通过引用字段关联文档,适合一对多关系
  4. 混合模式:结合嵌入和引用的优点,平衡性能和灵活性

设计原则

  1. 访问模式优先:根据查询需求设计文档结构
  2. 数据量考虑:避免单个文档过大(16MB限制)
  3. 更新频率:频繁更新的数据适合引用模式
  4. 原子性需求:需要原子操作的数据适合嵌入模式

常见模式

  1. 嵌入模式:用户配置、订单详情、文章评论
  2. 引用模式:用户订单、产品评论、社交关注
  3. 桶模式:时间序列数据、日志数据、传感器数据
  4. 属性模式:产品属性、用户配置、动态字段

最佳实践

  1. 文档大小:控制在1MB以内,避免超过10MB
  2. 嵌套深度:不超过3-4层,保持结构清晰
  3. 数组大小:限制数组元素数量,避免无限增长
  4. 索引优化:为常用查询字段创建索引

拓展参考资料

官方文档

推荐阅读

  • 《MongoDB数据建模实战》
  • 《NoSQL数据建模理论与实践》
  • 《MongoDB应用设计模式》

在线资源

  • MongoDB University数据建模课程
  • MongoDB官方博客数据建模文章
  • Stack Overflow MongoDB数据建模问题