Skip to content

4.6 常见错误与踩坑点

概述

在MongoDB CRUD操作中,开发者经常会遇到各种错误和陷阱。这些错误可能源于对MongoDB特性的误解、不当的操作方式、性能考虑不足等原因。本章节将总结MongoDB CRUD操作中的常见错误、踩坑点以及相应的解决方案和最佳实践。

理解这些常见错误和解决方案,可以帮助开发者避免重复犯错,提高开发效率,构建更加稳定和高效的MongoDB应用。

基本概念

错误分类

MongoDB CRUD操作中的错误可以分为以下几类:

  1. 语法错误:查询语法、更新操作符使用错误
  2. 类型错误:数据类型不匹配导致的错误
  3. 性能错误:不当操作导致的性能问题
  4. 逻辑错误:业务逻辑实现错误
  5. 配置错误:数据库配置不当导致的错误

常见错误模式

MongoDB CRUD操作中的常见错误模式:

  • 重复键错误:违反唯一索引约束
  • 文档大小超限:超过16MB文档大小限制
  • 字段名不规范:使用不符合BSON规范的字段名
  • 操作符使用错误:错误使用更新操作符
  • 查询条件错误:查询条件设置不当

错误处理策略

有效的错误处理策略:

  • 预防性检查:操作前验证数据和条件
  • 错误捕获:使用try-catch捕获异常
  • 错误恢复:实现错误恢复和重试机制
  • 错误日志:记录错误信息便于排查

原理深度解析

错误1:重复键错误

问题描述:插入或更新文档时违反唯一索引约束。

php
<?php
// 重复键错误详解
class DuplicateKeyErrorHandler {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function handleDuplicateKey($collectionName, $document) {
        $collection = $this->database->selectCollection($collectionName);
        
        try {
            $result = $collection->insertOne($document);
            
            return [
                'success' => true,
                'inserted_id' => $result->getInsertedId(),
                'message' => 'Document inserted successfully'
            ];
            
        } catch (MongoDB\Driver\Exception\BulkWriteException $e) {
            $writeResult = $e->getWriteResult();
            $writeErrors = $writeResult->getWriteErrors();
            
            foreach ($writeErrors as $error) {
                if ($error->getCode() === 11000) {
                    // 重复键错误
                    return $this->handleDuplicateKeyError($collectionName, $document, $error);
                }
            }
            
            return [
                'success' => false,
                'error' => 'Insertion failed',
                'message' => $e->getMessage()
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => 'Unexpected error',
                'message' => $e->getMessage()
            ];
        }
    }
    
    private function handleDuplicateKeyError($collectionName, $document, $error) {
        $collection = $this->database->selectCollection($collectionName);
        
        // 获取重复的键信息
        $errorMessage = $error->getMessage();
        $duplicateKey = $this->extractDuplicateKey($errorMessage);
        
        return [
            'success' => false,
            'error_type' => 'duplicate_key',
            'duplicate_key' => $duplicateKey,
            'message' => 'Document with this key already exists',
            'solutions' => [
                'update_existing' => 'Update the existing document instead of inserting',
                'use_upsert' => 'Use upsert option to update if exists',
                'ignore_duplicate' => 'Use ignore option to skip duplicate documents'
            ]
        ];
    }
    
    private function extractDuplicateKey($errorMessage) {
        // 从错误消息中提取重复的键名
        if (preg_match('/duplicate key error collection: \S+ index: (\S+)/', $errorMessage, $matches)) {
            return $matches[1];
        }
        
        return 'unknown';
    }
    
    public function insertWithUpsert($collectionName, $filter, $document) {
        $collection = $this->database->selectCollection($collectionName);
        
        try {
            $result = $collection->updateOne(
                $filter,
                [
                    '$set' => $document,
                    '$setOnInsert' => ['created_at' => new MongoDB\BSON\UTCDateTime()]
                ],
                ['upsert' => true]
            );
            
            $wasInserted = $result->getUpsertedCount() > 0;
            
            return [
                'success' => true,
                'action' => $wasInserted ? 'inserted' : 'updated',
                'upserted_id' => $result->getUpsertedId(),
                'modified_count' => $result->getModifiedCount()
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => 'Upsert operation failed',
                'message' => $e->getMessage()
            ];
        }
    }
}

// 使用示例
$duplicateKeyHandler = new DuplicateKeyErrorHandler('testdb');

// 尝试插入重复文档
$document = [
    'email' => 'john@example.com',
    'name' => 'John Doe'
];

$result = $duplicateKeyHandler->handleDuplicateKey('users', $document);
print_r($result);

// 使用upsert处理重复键
$upsertResult = $duplicateKeyHandler->insertWithUpsert(
    'users',
    ['email' => 'john@example.com'],
    ['name' => 'John Updated', 'updated_at' => new MongoDB\BSON\UTCDateTime()]
);
print_r($upsertResult);
?>

错误2:文档大小超限

问题描述:插入或更新文档时超过16MB大小限制。

php
<?php
// 文档大小超限错误处理
class DocumentSizeErrorHandler {
    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($document) {
        $documentSize = strlen(json_encode($document));
        $sizePercentage = ($documentSize / $this->maxDocumentSize) * 100;
        
        return [
            'document_size_bytes' => $documentSize,
            'document_size_mb' => round($documentSize / (1024 * 1024), 2),
            'size_percentage' => round($sizePercentage, 2),
            'status' => $this->getSizeStatus($documentSize),
            'recommendation' => $this->getSizeRecommendation($documentSize)
        ];
    }
    
    public function insertWithSizeCheck($collectionName, $document) {
        $collection = $this->database->selectCollection($collectionName);
        
        // 检查文档大小
        $sizeCheck = $this->checkDocumentSize($document);
        
        if ($sizeCheck['status'] === 'critical') {
            return [
                'success' => false,
                'error' => 'Document too large',
                'size_check' => $sizeCheck,
                'solutions' => [
                    'split_document' => 'Split the document into multiple smaller documents',
                    'use_gridfs' => 'Use GridFS for large files',
                    'reduce_data' => 'Reduce the amount of data in the document'
                ]
            ];
        }
        
        try {
            $result = $collection->insertOne($document);
            
            return [
                'success' => true,
                'inserted_id' => $result->getInsertedId(),
                'size_check' => $sizeCheck
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => 'Insertion failed',
                'message' => $e->getMessage(),
                'size_check' => $sizeCheck
            ];
        }
    }
    
    public function splitLargeDocument($collectionName, $document, $splitField, $maxSize = 5 * 1024 * 1024) {
        $collection = $this->database->selectCollection($collectionName);
        
        // 检查文档是否需要分割
        $documentSize = strlen(json_encode($document));
        
        if ($documentSize <= $maxSize) {
            // 文档大小合适,直接插入
            $result = $collection->insertOne($document);
            
            return [
                'success' => true,
                'action' => 'direct_insert',
                'inserted_id' => $result->getInsertedId()
            ];
        }
        
        // 分割文档
        if (!isset($document[$splitField]) || !is_array($document[$splitField])) {
            return [
                'success' => false,
                'error' => 'Split field not found or not an array',
                'split_field' => $splitField
            ];
        }
        
        $items = $document[$splitField];
        $baseDocument = $document;
        unset($baseDocument[$splitField]);
        
        // 分批插入
        $results = [
            'total_items' => count($items),
            'inserted_items' => 0,
            'base_document_id' => null,
            'item_document_ids' => []
        ];
        
        // 插入基础文档
        $baseResult = $collection->insertOne($baseDocument);
        $results['base_document_id'] = $baseResult->getInsertedId();
        
        // 分批插入项目
        $batchSize = 100;
        $batches = array_chunk($items, $batchSize);
        
        foreach ($batches as $batch) {
            $batchDocuments = [];
            foreach ($batch as $item) {
                $batchDocuments[] = array_merge($baseDocument, [
                    $splitField => $item,
                    'parent_id' => $results['base_document_id']
                ]);
            }
            
            $batchResult = $collection->insertMany($batchDocuments);
            $results['inserted_items'] += $batchResult->getInsertedCount();
            $results['item_document_ids'] = array_merge(
                $results['item_document_ids'],
                $batchResult->getInsertedIds()
            );
        }
        
        return [
            'success' => true,
            'action' => 'split_insert',
            'results' => $results
        ];
    }
    
    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 or reduce data.';
        } elseif ($size >= $this->warningThreshold) {
            return 'Document is approaching size limit. Consider optimization.';
        } else {
            return 'Document size is acceptable.';
        }
    }
}

// 使用示例
$documentSizeHandler = new DocumentSizeErrorHandler('testdb');

// 检查文档大小
$smallDocument = [
    'name' => 'Small Document',
    'data' => 'Small data'
];

$sizeCheck = $documentSizeHandler->checkDocumentSize($smallDocument);
print_r($sizeCheck);

// 创建大文档
$largeDocument = [
    'name' => 'Large Document',
    'data' => str_repeat('x', 20 * 1024 * 1024), // 20MB
    'items' => array_fill(0, 100, ['data' => str_repeat('y', 1024 * 1024)])
];

// 尝试插入大文档
$insertResult = $documentSizeHandler->insertWithSizeCheck('test_collection', $largeDocument);
print_r($insertResult);

// 分割大文档
$splitResult = $documentSizeHandler->splitLargeDocument(
    'test_collection',
    $largeDocument,
    'items'
);
print_r($splitResult);
?>

错误3:查询条件错误

问题描述:查询条件设置不当导致无法找到文档或返回错误结果。

php
<?php
// 查询条件错误处理
class QueryConditionErrorHandler {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function validateQueryCondition($filter) {
        $validationErrors = [];
        
        // 检查空条件
        if (empty($filter)) {
            $validationErrors[] = 'Empty filter will match all documents';
        }
        
        // 检查操作符使用
        $this->validateOperators($filter, $validationErrors);
        
        // 检查字段类型
        $this->validateFieldTypes($filter, $validationErrors);
        
        return [
            'valid' => empty($validationErrors),
            'errors' => $validationErrors
        ];
    }
    
    private function validateOperators($filter, &$errors) {
        foreach ($filter as $key => $value) {
            if (is_array($value) && isset($value['$in'])) {
                // 检查$in操作符
                if (!is_array($value['$in']) || empty($value['$in'])) {
                    $errors[] = "Operator \$in requires non-empty array for field '{$key}'";
                }
            }
            
            if (is_array($value) && isset($value['$nin'])) {
                // 检查$nin操作符
                if (!is_array($value['$nin']) || empty($value['$nin'])) {
                    $errors[] = "Operator \$nin requires non-empty array for field '{$key}'";
                }
            }
        }
    }
    
    private function validateFieldTypes($filter, &$errors) {
        foreach ($filter as $key => $value) {
            if (is_array($value) && isset($value['$regex'])) {
                // 检查正则表达式
                if (!($value['$regex'] instanceof MongoDB\BSON\Regex)) {
                    $errors[] = "Field '{$key}' requires MongoDB\BSON\Regex object for \$regex operator";
                }
            }
            
            if (is_array($value) && isset($value['$type'])) {
                // 检查类型操作符
                $validTypes = ['string', 'object', 'array', 'binData', 'undefined', 'bool', 'int', 'long', 'double', 'decimal', 'date', 'null'];
                if (!in_array($value['$type'], $validTypes)) {
                    $errors[] = "Invalid type '{$value['$type']}' for field '{$key}'";
                }
            }
        }
    }
    
    public function queryWithValidation($collectionName, $filter, $options = []) {
        $collection = $this->database->selectCollection($collectionName);
        
        // 验证查询条件
        $validation = $this->validateQueryCondition($filter);
        
        if (!$validation['valid']) {
            return [
                'success' => false,
                'error' => 'Invalid query condition',
                'validation_errors' => $validation['errors']
            ];
        }
        
        try {
            $cursor = $collection->find($filter, $options);
            $documents = iterator_to_array($cursor);
            
            return [
                'success' => true,
                'document_count' => count($documents),
                'documents' => $documents
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => 'Query failed',
                'message' => $e->getMessage()
            ];
        }
    }
    
    public function suggestQueryOptimization($collectionName, $filter) {
        $collection = $this->database->selectCollection($collectionName);
        
        $suggestions = [];
        
        // 检查是否可以使用索引
        $indexedFields = $this->getIndexedFields($collectionName);
        foreach ($filter as $field => $value) {
            if (in_array($field, $indexedFields)) {
                $suggestions[] = [
                    'type' => 'index_usage',
                    'message' => "Field '{$field}' has an index, consider using it in query",
                    'field' => $field
                ];
            }
        }
        
        // 检查是否可以使用投影
        if (!isset($options['projection'])) {
            $suggestions[] = [
                'type' => 'projection',
                'message' => 'Consider using projection to limit returned fields',
                'recommendation' => 'Only return required fields to improve performance'
            ];
        }
        
        // 检查是否可以使用限制
        if (!isset($options['limit'])) {
            $suggestions[] = [
                'type' => 'limit',
                'message' => 'Consider using limit to restrict result set size',
                'recommendation' => 'Use pagination for large result sets'
            ];
        }
        
        return $suggestions;
    }
    
    private function getIndexedFields($collectionName) {
        $collection = $this->database->selectCollection($collectionName);
        
        $indexes = $collection->listIndexes();
        $indexedFields = [];
        
        foreach ($indexes as $index) {
            $key = $index->getKey();
            foreach ($key as $field => $direction) {
                $indexedFields[] = $field;
            }
        }
        
        return array_unique($indexedFields);
    }
}

// 使用示例
$queryErrorHandler = new QueryConditionErrorHandler('testdb');

// 验证查询条件
$filter = [
    'email' => 'john@example.com',
    'age' => ['$gte' => 25, '$lte' => 35],
    'status' => 'active'
];

$validation = $queryErrorHandler->validateQueryCondition($filter);
print_r($validation);

// 带验证的查询
$queryResult = $queryErrorHandler->queryWithValidation(
    'users',
    $filter,
    ['projection' => ['name' => 1, 'email' => 1, 'age' => 1], 'limit' => 10]
);
print_r($queryResult);

// 获取查询优化建议
$optimizations = $queryErrorHandler->suggestQueryOptimization('users', $filter);
print_r($optimizations);
?>

错误4:更新操作符使用错误

问题描述:错误使用更新操作符导致更新失败或产生意外结果。

php
<?php
// 更新操作符错误处理
class UpdateOperatorErrorHandler {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    public function validateUpdateOperator($update) {
        $validationErrors = [];
        
        // 检查$set操作符
        if (isset($update['$set'])) {
            $this->validateSetOperator($update['$set'], $validationErrors);
        }
        
        // 检查$inc操作符
        if (isset($update['$inc'])) {
            $this->validateIncOperator($update['$inc'], $validationErrors);
        }
        
        // 检查数组操作符
        if (isset($update['$push']) || isset($update['$pull']) || isset($update['$addToSet'])) {
            $this->validateArrayOperators($update, $validationErrors);
        }
        
        return [
            'valid' => empty($validationErrors),
            'errors' => $validationErrors
        ];
    }
    
    private function validateSetOperator($set, &$errors) {
        foreach ($set as $field => $value) {
            // 检查是否嵌套操作符
            if (is_array($value)) {
                foreach ($value as $key => $val) {
                    if (str_starts_with($key, '$')) {
                        $errors[] = "Nested operator '{$key}' found in \$set for field '{$field}'";
                    }
                }
            }
        }
    }
    
    private function validateIncOperator($inc, &$errors) {
        foreach ($inc as $field => $value) {
            // 检查值是否为数字
            if (!is_numeric($value)) {
                $errors[] = "Operator \$inc requires numeric value for field '{$field}'";
            }
        }
    }
    
    private function validateArrayOperators($update, &$errors) {
        // 检查$push操作符
        if (isset($update['$push'])) {
            foreach ($update['$push'] as $field => $value) {
                if (isset($value['$each']) && !is_array($value['$each'])) {
                    $errors[] = "Operator \$push with \$each requires array for field '{$field}'";
                }
            }
        }
        
        // 检查$pull操作符
        if (isset($update['$pull'])) {
            foreach ($update['$pull'] as $field => $value) {
                if (isset($value['$in']) && !is_array($value['$in'])) {
                    $errors[] = "Operator \$pull with \$in requires array for field '{$field}'";
                }
            }
        }
    }
    
    public function updateWithValidation($collectionName, $filter, $update, $options = []) {
        $collection = $this->database->selectCollection($collectionName);
        
        // 验证更新操作符
        $validation = $this->validateUpdateOperator($update);
        
        if (!$validation['valid']) {
            return [
                'success' => false,
                'error' => 'Invalid update operator',
                'validation_errors' => $validation['errors']
            ];
        }
        
        try {
            $result = $collection->updateOne($filter, $update, $options);
            
            return [
                'success' => true,
                'matched_count' => $result->getMatchedCount(),
                'modified_count' => $result->getModifiedCount()
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => 'Update failed',
                'message' => $e->getMessage()
            ];
        }
    }
    
    public function suggestUpdateOptimization($update) {
        $suggestions = [];
        
        // 检查是否可以使用$inc代替$set
        if (isset($update['$set'])) {
            foreach ($update['$set'] as $field => $value) {
                if (is_numeric($value) && $value > 0) {
                    $suggestions[] = [
                        'type' => 'operator_optimization',
                        'message' => "Consider using \$inc instead of \$set for field '{$field}'",
                        'current' => "\$set: {$field} => {$value}",
                        'suggested' => "\$inc: {$field} => {$value}"
                    ];
                }
            }
        }
        
        // 检查是否可以使用$unset代替$set
        if (isset($update['$set'])) {
            foreach ($update['$set'] as $field => $value) {
                if ($value === null) {
                    $suggestions[] = [
                        'type' => 'operator_optimization',
                        'message' => "Consider using \$unset instead of \$set with null for field '{$field}'",
                        'current' => "\$set: {$field} => null",
                        'suggested' => "\$unset: {$field} => 1"
                    ];
                }
            }
        }
        
        return $suggestions;
    }
}

// 使用示例
$updateOperatorErrorHandler = new UpdateOperatorErrorHandler('testdb');

// 验证更新操作符
$update = [
    '$set' => [
        'name' => 'John Updated',
        'age' => 31,
        'updated_at' => new MongoDB\BSON\UTCDateTime()
    ],
    '$inc' => [
        'login_count' => 1
    ]
];

$validation = $updateOperatorErrorHandler->validateUpdateOperator($update);
print_r($validation);

// 带验证的更新
$updateResult = $updateOperatorErrorHandler->updateWithValidation(
    'users',
    ['email' => 'john@example.com'],
    $update
);
print_r($updateResult);

// 获取更新优化建议
$optimizations = $updateOperatorErrorHandler->suggestUpdateOptimization($update);
print_r($optimizations);
?>

常见应用场景

场景1:CRUD操作错误处理系统

实现完整的CRUD操作错误处理系统:

php
<?php
// CRUD操作错误处理系统
class CRUDOperationErrorHandler {
    private $database;
    private $errorLog;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
        $this->errorLog = $this->database->selectCollection('error_logs');
    }
    
    public function safeInsert($collectionName, $document, $options = []) {
        $collection = $this->database->selectCollection($collectionName);
        
        try {
            // 验证文档
            $validation = $this->validateDocument($document);
            if (!$validation['valid']) {
                throw new Exception('Document validation failed: ' . implode(', ', $validation['errors']));
            }
            
            // 检查文档大小
            $sizeCheck = $this->checkDocumentSize($document);
            if ($sizeCheck['status'] === 'critical') {
                throw new Exception('Document size exceeds limit');
            }
            
            // 执行插入
            $result = $collection->insertOne($document, $options);
            
            return [
                'success' => true,
                'inserted_id' => $result->getInsertedId(),
                'validation' => $validation,
                'size_check' => $sizeCheck
            ];
            
        } catch (MongoDB\Driver\Exception\BulkWriteException $e) {
            $errorInfo = $this->handleBulkWriteError($e);
            $this->logError('insert', $collectionName, $document, $errorInfo);
            
            return [
                'success' => false,
                'error_type' => 'bulk_write_error',
                'error_info' => $errorInfo
            ];
            
        } catch (Exception $e) {
            $errorInfo = [
                'message' => $e->getMessage(),
                'code' => $e->getCode()
            ];
            
            $this->logError('insert', $collectionName, $document, $errorInfo);
            
            return [
                'success' => false,
                'error_type' => 'exception',
                'error_info' => $errorInfo
            ];
        }
    }
    
    public function safeUpdate($collectionName, $filter, $update, $options = []) {
        $collection = $this->database->selectCollection($collectionName);
        
        try {
            // 验证更新操作符
            $validation = $this->validateUpdateOperator($update);
            if (!$validation['valid']) {
                throw new Exception('Update operator validation failed: ' . implode(', ', $validation['errors']));
            }
            
            // 执行更新
            $result = $collection->updateOne($filter, $update, $options);
            
            return [
                'success' => true,
                'matched_count' => $result->getMatchedCount(),
                'modified_count' => $result->getModifiedCount(),
                'validation' => $validation
            ];
            
        } catch (MongoDB\Driver\Exception\BulkWriteException $e) {
            $errorInfo = $this->handleBulkWriteError($e);
            $this->logError('update', $collectionName, $filter, $errorInfo);
            
            return [
                'success' => false,
                'error_type' => 'bulk_write_error',
                'error_info' => $errorInfo
            ];
            
        } catch (Exception $e) {
            $errorInfo = [
                'message' => $e->getMessage(),
                'code' => $e->getCode()
            ];
            
            $this->logError('update', $collectionName, $filter, $errorInfo);
            
            return [
                'success' => false,
                'error_type' => 'exception',
                'error_info' => $errorInfo
            ];
        }
    }
    
    public function safeDelete($collectionName, $filter, $options = []) {
        $collection = $this->database->selectCollection($collectionName);
        
        try {
            // 验证删除条件
            $validation = $this->validateDeleteCondition($filter);
            if (!$validation['valid']) {
                throw new Exception('Delete condition validation failed: ' . implode(', ', $validation['errors']));
            }
            
            // 执行删除
            $result = $collection->deleteOne($filter, $options);
            
            return [
                'success' => true,
                'deleted_count' => $result->getDeletedCount(),
                'validation' => $validation
            ];
            
        } catch (Exception $e) {
            $errorInfo = [
                'message' => $e->getMessage(),
                'code' => $e->getCode()
            ];
            
            $this->logError('delete', $collectionName, $filter, $errorInfo);
            
            return [
                'success' => false,
                'error_type' => 'exception',
                'error_info' => $errorInfo
            ];
        }
    }
    
    private function validateDocument($document) {
        $errors = [];
        
        // 检查必填字段
        if (empty($document['email'])) {
            $errors[] = 'Email field is required';
        }
        
        // 检查字段类型
        if (isset($document['age']) && !is_int($document['age'])) {
            $errors[] = 'Age field must be an integer';
        }
        
        return [
            'valid' => empty($errors),
            'errors' => $errors
        ];
    }
    
    private function validateUpdateOperator($update) {
        $errors = [];
        
        // 检查$set操作符
        if (isset($update['$set'])) {
            foreach ($update['$set'] as $field => $value) {
                if (is_array($value) && !empty($value)) {
                    foreach ($value as $key => $val) {
                        if (str_starts_with($key, '$')) {
                            $errors[] = "Nested operator '{$key}' in \$set for field '{$field}'";
                        }
                    }
                }
            }
        }
        
        return [
            'valid' => empty($errors),
            'errors' => $errors
        ];
    }
    
    private function validateDeleteCondition($filter) {
        $errors = [];
        
        // 检查空条件
        if (empty($filter)) {
            $errors[] = 'Empty delete condition will delete all documents';
        }
        
        return [
            'valid' => empty($errors),
            'errors' => $errors
        ];
    }
    
    private function checkDocumentSize($document) {
        $documentSize = strlen(json_encode($document));
        $maxSize = 16 * 1024 * 1024; // 16MB
        
        return [
            'size_bytes' => $documentSize,
            'size_mb' => round($documentSize / (1024 * 1024), 2),
            'status' => $documentSize >= $maxSize ? 'critical' : 'normal'
        ];
    }
    
    private function handleBulkWriteError($e) {
        $writeResult = $e->getWriteResult();
        $writeErrors = $writeResult->getWriteErrors();
        
        $errorInfo = [
            'write_errors' => [],
            'inserted_count' => $writeResult->getInsertedCount(),
            'modified_count' => $writeResult->getModifiedCount(),
            'deleted_count' => $writeResult->getDeletedCount()
        ];
        
        foreach ($writeErrors as $error) {
            $errorInfo['write_errors'][] = [
                'code' => $error->getCode(),
                'message' => $error->getMessage(),
                'index' => $error->getIndex()
            ];
        }
        
        return $errorInfo;
    }
    
    private function logError($operation, $collectionName, $data, $errorInfo) {
        $this->errorLog->insertOne([
            'operation' => $operation,
            'collection' => $collectionName,
            'data' => $data,
            'error_info' => $errorInfo,
            'timestamp' => new MongoDB\BSON\UTCDateTime(),
            'server' => gethostname(),
            'process_id' => getmypid()
        ]);
    }
}

// 使用示例
$crudErrorHandler = new CRUDOperationErrorHandler('testdb');

// 安全插入
$document = [
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'age' => 30
];

$insertResult = $crudErrorHandler->safeInsert('users', $document);
print_r($insertResult);

// 安全更新
$update = [
    '$set' => ['name' => 'John Updated', 'updated_at' => new MongoDB\BSON\UTCDateTime()],
    '$inc' => ['login_count' => 1]
];

$updateResult = $crudErrorHandler->safeUpdate('users', ['email' => 'john@example.com'], $update);
print_r($updateResult);

// 安全删除
$deleteResult = $crudErrorHandler->safeDelete('users', ['email' => 'john@example.com']);
print_r($deleteResult);
?>

常见问题答疑

问题1:如何避免重复键错误?

回答:通过检查存在性、使用upsert、唯一索引等方式避免:

php
<?php
// 避免重复键错误助手
class DuplicateKeyPreventer {
    public static function preventDuplicateKey($collection, $document, $uniqueFields) {
        $filter = [];
        
        // 构建唯一字段过滤条件
        foreach ($uniqueFields as $field) {
            if (isset($document[$field])) {
                $filter[$field] = $document[$field];
            }
        }
        
        if (empty($filter)) {
            return [
                'recommendation' => 'insert',
                'message' => 'No unique fields specified, proceed with insert'
            ];
        }
        
        // 检查文档是否存在
        $existingDocument = $collection->findOne($filter);
        
        if ($existingDocument) {
            return [
                'recommendation' => 'update',
                'message' => 'Document exists, update instead of insert',
                'existing_document' => $existingDocument
            ];
        }
        
        return [
            'recommendation' => 'insert',
            'message' => 'Document does not exist, safe to insert'
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->selectDatabase("testdb");
$collection = $database->selectCollection('users');

$document = [
    'email' => 'john@example.com',
    'name' => 'John Doe'
];

$prevention = DuplicateKeyPreventer::preventDuplicateKey($collection, $document, ['email']);
print_r($prevention);
?>

实战练习

练习1:实现完整的CRUD错误处理系统

实现一个完整的CRUD操作错误处理和恢复系统:

php
<?php
// 完整的CRUD错误处理和恢复系统
class CompleteCRUDErrorHandler {
    private $database;
    private $retryConfig;
    
    public function __construct($databaseName, $retryConfig = []) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
        
        $this->retryConfig = [
            'max_retries' => $retryConfig['max_retries'] ?? 3,
            'retry_delay' => $retryConfig['retry_delay'] ?? 100000, // 100ms
            'retryable_errors' => $retryConfig['retryable_errors'] ?? [11000] // 重复键错误
        ];
    }
    
    public function insertWithRetry($collectionName, $document, $options = []) {
        $collection = $this->database->selectCollection($collectionName);
        
        $attempt = 0;
        $lastError = null;
        
        while ($attempt < $this->retryConfig['max_retries']) {
            $attempt++;
            
            try {
                $result = $collection->insertOne($document, $options);
                
                return [
                    'success' => true,
                    'inserted_id' => $result->getInsertedId(),
                    'attempts' => $attempt
                ];
                
            } catch (MongoDB\Driver\Exception\BulkWriteException $e) {
                $lastError = $e;
                $writeErrors = $e->getWriteResult()->getWriteErrors();
                
                // 检查是否可重试的错误
                $isRetryable = false;
                foreach ($writeErrors as $error) {
                    if (in_array($error->getCode(), $this->retryConfig['retryable_errors'])) {
                        $isRetryable = true;
                        break;
                    }
                }
                
                if (!$isRetryable) {
                    break;
                }
                
                // 等待后重试
                usleep($this->retryConfig['retry_delay']);
                
            } catch (Exception $e) {
                $lastError = $e;
                break;
            }
        }
        
        return [
            'success' => false,
            'error' => 'Insertion failed after retries',
            'attempts' => $attempt,
            'last_error' => $lastError ? $lastError->getMessage() : 'Unknown error'
        ];
    }
    
    public function updateWithRetry($collectionName, $filter, $update, $options = []) {
        $collection = $this->database->selectCollection($collectionName);
        
        $attempt = 0;
        $lastError = null;
        
        while ($attempt < $this->retryConfig['max_retries']) {
            $attempt++;
            
            try {
                $result = $collection->updateOne($filter, $update, $options);
                
                return [
                    'success' => true,
                    'matched_count' => $result->getMatchedCount(),
                    'modified_count' => $result->getModifiedCount(),
                    'attempts' => $attempt
                ];
                
            } catch (MongoDB\Driver\Exception\BulkWriteException $e) {
                $lastError = $e;
                $writeErrors = $e->getWriteResult()->getWriteErrors();
                
                // 检查是否可重试的错误
                $isRetryable = false;
                foreach ($writeErrors as $error) {
                    if (in_array($error->getCode(), $this->retryConfig['retryable_errors'])) {
                        $isRetryable = true;
                        break;
                    }
                }
                
                if (!$isRetryable) {
                    break;
                }
                
                // 等待后重试
                usleep($this->retryConfig['retry_delay']);
                
            } catch (Exception $e) {
                $lastError = $e;
                break;
            }
        }
        
        return [
            'success' => false,
            'error' => 'Update failed after retries',
            'attempts' => $attempt,
            'last_error' => $lastError ? $lastError->getMessage() : 'Unknown error'
        ];
    }
}

// 使用示例
$crudErrorHandler = new CompleteCRUDErrorHandler('testdb', [
    'max_retries' => 3,
    'retry_delay' => 100000,
    'retryable_errors' => [11000]
]);

// 带重试的插入
$document = [
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'age' => 30
];

$insertResult = $crudErrorHandler->insertWithRetry('users', $document);
print_r($insertResult);

// 带重试的更新
$update = [
    '$set' => ['name' => 'John Updated', 'updated_at' => new MongoDB\BSON\UTCDateTime()],
    '$inc' => ['login_count' => 1]
];

$updateResult = $crudErrorHandler->updateWithRetry('users', ['email' => 'john@example.com'], $update);
print_r($updateResult);
?>

知识点总结

核心错误类型

  1. 重复键错误:违反唯一索引约束
  2. 文档大小超限:超过16MB大小限制
  3. 查询条件错误:查询条件设置不当
  4. 操作符使用错误:错误使用更新操作符

错误处理策略

  1. 预防性检查:操作前验证数据和条件
  2. 错误捕获:使用try-catch捕获异常
  3. 错误恢复:实现错误恢复和重试机制
  4. 错误日志:记录错误信息便于排查

最佳实践

  1. 验证数据:操作前验证数据完整性和正确性
  2. 检查大小:确保文档大小在限制范围内
  3. 使用upsert:使用upsert避免重复键错误
  4. 错误重试:对可重试的错误实现重试机制

常见解决方案

  1. 重复键:使用upsert或检查存在性
  2. 文档大小:分割文档或使用GridFS
  3. 查询条件:验证查询条件和操作符
  4. 更新操作符:正确使用更新操作符

拓展参考资料

官方文档

推荐阅读

  • 《MongoDB错误处理指南》
  • 《MongoDB性能优化实践》
  • 《MongoDB数据完整性保障》

在线资源

  • MongoDB University错误处理课程
  • MongoDB官方博客错误处理文章
  • Stack Overflow MongoDB错误相关问题