Appearance
3.3 文档结构设计
概述
文档结构设计是MongoDB数据建模的核心,直接影响应用的性能、可维护性和扩展性。与关系型数据库的表结构设计不同,MongoDB的文档结构更加灵活,支持嵌套文档、数组、混合数据类型等特性。本章节将深入探讨MongoDB文档结构设计的原则、模式和实践技巧。
良好的文档结构设计需要考虑数据的访问模式、查询需求、更新频率、数据量大小等多个因素。通过合理的文档结构设计,可以优化查询性能、减少数据冗余、简化应用逻辑、提高系统的整体效率。
基本概念
文档结构定义
MongoDB文档是BSON格式的二进制表示,具有以下特点:
- 键值对结构:文档由字段和值组成,类似于JSON对象
- 嵌套能力:支持多层嵌套文档和数组
- 动态模式:同一集合中的文档可以有不同的字段
- 类型丰富:支持多种数据类型,包括字符串、数字、日期、二进制数据等
- 大小限制:单个文档最大16MB
文档设计原则
- 嵌入与引用:根据数据关系选择嵌入或引用模式
- 数据原子性:考虑更新操作的原子性需求
- 查询性能:根据查询模式优化文档结构
- 数据增长:预测文档大小增长,避免超出限制
- 读写比例:根据读写比例选择合适的数据组织方式
常见文档模式
- 嵌入模式:将相关数据嵌入到同一文档中
- 引用模式:通过引用字段关联其他文档
- 混合模式:结合嵌入和引用的优点
- 桶模式:将时间序列数据分组存储
- 属性模式:使用对象数组存储可变属性
原理深度解析
嵌入模式原理
嵌入模式将相关数据存储在同一个文档中,减少查询次数:
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);
?>知识点总结
核心概念
- 文档结构:MongoDB使用BSON格式存储文档,支持嵌套和数组
- 嵌入模式:将相关数据存储在同一文档中,适合一对少关系
- 引用模式:通过引用字段关联文档,适合一对多关系
- 混合模式:结合嵌入和引用的优点,平衡性能和灵活性
设计原则
- 访问模式优先:根据查询需求设计文档结构
- 数据量考虑:避免单个文档过大(16MB限制)
- 更新频率:频繁更新的数据适合引用模式
- 原子性需求:需要原子操作的数据适合嵌入模式
常见模式
- 嵌入模式:用户配置、订单详情、文章评论
- 引用模式:用户订单、产品评论、社交关注
- 桶模式:时间序列数据、日志数据、传感器数据
- 属性模式:产品属性、用户配置、动态字段
最佳实践
- 文档大小:控制在1MB以内,避免超过10MB
- 嵌套深度:不超过3-4层,保持结构清晰
- 数组大小:限制数组元素数量,避免无限增长
- 索引优化:为常用查询字段创建索引
拓展参考资料
官方文档
- MongoDB数据建模:https://docs.mongodb.com/manual/core/data-modeling/
- 文档结构设计:https://docs.mongodb.com/manual/core/document/
- 嵌入与引用:https://docs.mongodb.com/manual/core/data-model-embedded-vs-reference/
推荐阅读
- 《MongoDB数据建模实战》
- 《NoSQL数据建模理论与实践》
- 《MongoDB应用设计模式》
在线资源
- MongoDB University数据建模课程
- MongoDB官方博客数据建模文章
- Stack Overflow MongoDB数据建模问题
