Appearance
4.6 常见错误与踩坑点
概述
在MongoDB CRUD操作中,开发者经常会遇到各种错误和陷阱。这些错误可能源于对MongoDB特性的误解、不当的操作方式、性能考虑不足等原因。本章节将总结MongoDB CRUD操作中的常见错误、踩坑点以及相应的解决方案和最佳实践。
理解这些常见错误和解决方案,可以帮助开发者避免重复犯错,提高开发效率,构建更加稳定和高效的MongoDB应用。
基本概念
错误分类
MongoDB CRUD操作中的错误可以分为以下几类:
- 语法错误:查询语法、更新操作符使用错误
- 类型错误:数据类型不匹配导致的错误
- 性能错误:不当操作导致的性能问题
- 逻辑错误:业务逻辑实现错误
- 配置错误:数据库配置不当导致的错误
常见错误模式
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);
?>知识点总结
核心错误类型
- 重复键错误:违反唯一索引约束
- 文档大小超限:超过16MB大小限制
- 查询条件错误:查询条件设置不当
- 操作符使用错误:错误使用更新操作符
错误处理策略
- 预防性检查:操作前验证数据和条件
- 错误捕获:使用try-catch捕获异常
- 错误恢复:实现错误恢复和重试机制
- 错误日志:记录错误信息便于排查
最佳实践
- 验证数据:操作前验证数据完整性和正确性
- 检查大小:确保文档大小在限制范围内
- 使用upsert:使用upsert避免重复键错误
- 错误重试:对可重试的错误实现重试机制
常见解决方案
- 重复键:使用upsert或检查存在性
- 文档大小:分割文档或使用GridFS
- 查询条件:验证查询条件和操作符
- 更新操作符:正确使用更新操作符
拓展参考资料
官方文档
- MongoDB错误代码:https://docs.mongodb.com/manual/reference/error-codes/
- 写入关注级别:https://docs.mongodb.com/manual/reference/write-concern/
- 错误处理最佳实践:https://docs.mongodb.com/manual/core/error-handling/
推荐阅读
- 《MongoDB错误处理指南》
- 《MongoDB性能优化实践》
- 《MongoDB数据完整性保障》
在线资源
- MongoDB University错误处理课程
- MongoDB官方博客错误处理文章
- Stack Overflow MongoDB错误相关问题
