Skip to content

投影与字段选择

概述

投影(Projection)是MongoDB查询中的重要功能,它允许开发者指定返回文档中包含或排除的字段。通过合理使用投影,可以显著减少网络传输数据量,提高查询性能,并简化客户端数据处理。PHP MongoDB驱动提供了灵活的投影语法,支持字段包含、排除、数组元素过滤等多种投影操作。

投影操作不仅影响性能,还关系到数据安全和隐私保护。通过投影,可以确保只返回必要的字段,避免敏感数据泄露。掌握投影的各种用法和最佳实践,对于构建高效、安全的MongoDB应用至关重要。

基本概念

字段包含投影

指定只返回特定字段:

  • 包含字段:使用字段名: 1语法
  • 默认包含_id字段默认总是返回
  • 排除_id:使用_id: 0排除
  • 混合模式:不能同时使用包含和排除(除_id外)

字段排除投影

指定排除特定字段:

  • 排除字段:使用字段名: 0语法
  • 排除多个字段:同时排除多个字段
  • 安全性:用于隐藏敏感信息

数组投影

控制数组字段的返回:

  • 切片投影$slice操作符
  • 元素匹配$elemMatch操作符
  • 数组过滤$位置操作符

聚合投影

在聚合管道中使用投影:

  • 计算字段:添加计算得出的字段
  • 重命名字段:改变字段名称
  • 条件投影:基于条件返回字段

原理深度解析

投影执行机制

MongoDB投影操作的内部执行过程:

php
class ProjectionExecutionMechanism {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function analyzeProjectionExecution($filter, $projection) {
        $command = new MongoDB\Driver\Command([
            'explain' => $this->collection->getNamespace() . '.find',
            'filter' => $filter,
            'projection' => $projection,
            'verbosity' => 'executionStats'
        ]);
        
        $cursor = $this->collection->getManager()->executeCommand(
            $this->collection->getDatabaseName(),
            $command
        );
        
        $result = $cursor->toArray()[0];
        $executionStats = $result['executionStats'];
        
        return [
            'projection_type' => $this->determineProjectionType($projection),
            'docs_returned' => $executionStats['totalDocsExamined'],
            'execution_time_ms' => $executionStats['executionTimeMillis'],
            'projection_stage' => $result['queryPlanner']['winningPlan']['inputStage']['stage'] ?? null
        ];
    }
    
    private function determineProjectionType($projection) {
        $hasInclusion = false;
        $hasExclusion = false;
        
        foreach ($projection as $field => $value) {
            if ($field === '_id') continue;
            
            if ($value === 1) {
                $hasInclusion = true;
            } elseif ($value === 0) {
                $hasExclusion = true;
            }
        }
        
        if ($hasInclusion && $hasExclusion) {
            return 'mixed';
        } elseif ($hasInclusion) {
            return 'inclusion';
        } elseif ($hasExclusion) {
            return 'exclusion';
        }
        
        return 'none';
    }
    
    public function compareProjectionPerformance($filter, $projections) {
        $results = [];
        
        foreach ($projections as $name => $projection) {
            $start = microtime(true);
            $analysis = $this->analyzeProjectionExecution($filter, $projection);
            $time = (microtime(true) - $start) * 1000;
            
            $results[$name] = [
                'execution_time_ms' => $time,
                'projection' => $projection,
                'docs_returned' => $analysis['docs_returned'],
                'data_size_bytes' => $this->measureDataSize($filter, $projection)
            ];
        }
        
        return $results;
    }
    
    private function measureDataSize($filter, $projection) {
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        $totalSize = 0;
        
        foreach ($documents as $document) {
            $totalSize += strlen(json_encode($document));
        }
        
        return $totalSize;
    }
}

投影与索引覆盖

投影对索引覆盖查询的影响:

php
class ProjectionIndexCoverage {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function analyzeIndexCoverage($filter, $projection, $sort = []) {
        $command = new MongoDB\Driver\Command([
            'explain' => $this->collection->getNamespace() . '.find',
            'filter' => $filter,
            'projection' => $projection,
            'sort' => $sort,
            'verbosity' => 'executionStats'
        ]);
        
        $cursor = $this->collection->getManager()->executeCommand(
            $this->collection->getDatabaseName(),
            $command
        );
        
        $result = $cursor->toArray()[0];
        $executionStats = $result['executionStats'];
        $winningPlan = $result['queryPlanner']['winningPlan'];
        
        $coverage = [
            'is_covered' => false,
            'index_used' => null,
            'docs_examined' => $executionStats['totalDocsExamined'],
            'keys_examined' => $executionStats['totalKeysExamined'],
            'execution_time_ms' => $executionStats['executionTimeMillis']
        ];
        
        if (isset($winningPlan['inputStage']['stage']) && 
            $winningPlan['inputStage']['stage'] === 'FETCH') {
            $coverage['is_covered'] = false;
        } elseif (isset($winningPlan['inputStage']['stage']) && 
                  $winningPlan['inputStage']['stage'] === 'IXSCAN') {
            $coverage['is_covered'] = true;
            $coverage['index_used'] = $winningPlan['inputStage']['indexName'];
        }
        
        return $coverage;
    }
    
    public function createCoveringIndex($filterFields, $projectionFields, $sortFields = []) {
        $indexFields = [];
        
        foreach ($filterFields as $field) {
            $indexFields[$field] = 1;
        }
        
        foreach ($sortFields as $field => $direction) {
            $indexFields[$field] = $direction;
        }
        
        foreach ($projectionFields as $field) {
            if ($field !== '_id' && !isset($indexFields[$field])) {
                $indexFields[$field] = 1;
            }
        }
        
        $indexName = 'covering_' . md5(json_encode($indexFields));
        
        try {
            $result = $this->collection->createIndex($indexFields, ['name' => $indexName]);
            return [
                'success' => true,
                'index_name' => $indexName,
                'index_fields' => $indexFields
            ];
        } catch (MongoDB\Driver\Exception\Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    public function optimizeProjectionForCoverage($filter, $projection, $sort = []) {
        $currentCoverage = $this->analyzeIndexCoverage($filter, $projection, $sort);
        
        if ($currentCoverage['is_covered']) {
            return [
                'already_optimized' => true,
                'current_index' => $currentCoverage['index_used']
            ];
        }
        
        $filterFields = $this->extractFieldsFromFilter($filter);
        $projectionFields = array_keys($projection);
        $sortFields = $sort;
        
        $indexResult = $this->createCoveringIndex($filterFields, $projectionFields, $sortFields);
        
        if ($indexResult['success']) {
            $newCoverage = $this->analyzeIndexCoverage($filter, $projection, $sort);
            
            return [
                'already_optimized' => false,
                'index_created' => $indexResult,
                'new_coverage' => $newCoverage
            ];
        }
        
        return [
            'already_optimized' => false,
            'index_creation_failed' => $indexResult
        ];
    }
    
    private function extractFieldsFromFilter($filter) {
        $fields = [];
        
        foreach ($filter as $key => $value) {
            if (!in_array($key, ['$and', '$or', '$not'])) {
                $fields[] = $key;
            } elseif (is_array($value)) {
                foreach ($value as $subFilter) {
                    $fields = array_merge($fields, $this->extractFieldsFromFilter($subFilter));
                }
            }
        }
        
        return array_unique($fields);
    }
}

复杂投影操作

处理复杂的投影需求:

php
class ComplexProjectionOperations {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function arraySliceProjection($filter, $arrayField, $skip, $limit) {
        $projection = [
            $arrayField => ['$slice' => [$skip, $limit]]
        ];
        
        return $this->collection->find($filter, ['projection' => $projection]);
    }
    
    public function arrayElementMatchProjection($filter, $arrayField, $matchCondition) {
        $projection = [
            $arrayField => ['$elemMatch' => $matchCondition]
        ];
        
        return $this->collection->find($filter, ['projection' => $projection]);
    }
    
    public function computedFieldProjection($filter, $computedFields) {
        $pipeline = [
            ['$match' => $filter],
            ['$project' => $computedFields]
        ];
        
        return $this->collection->aggregate($pipeline);
    }
    
    public function conditionalProjection($filter, $conditions) {
        $pipeline = [
            ['$match' => $filter],
            ['$project' => $conditions]
        ];
        
        return $this->collection->aggregate($pipeline);
    }
    
    public function nestedFieldProjection($filter, $nestedFields) {
        $projection = [];
        
        foreach ($nestedFields as $field) {
            $projection[$field] = 1;
        }
        
        return $this->collection->find($filter, ['projection' => $projection]);
    }
    
    public function dynamicProjection($filter, $fieldSelector) {
        $projection = [];
        
        foreach ($fieldSelector as $field => $include) {
            $projection[$field] = $include ? 1 : 0;
        }
        
        return $this->collection->find($filter, ['projection' => $projection]);
    }
}

常见错误与踩坑点

投影语法错误

常见的投影语法错误:

php
class ProjectionErrorHandler {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function validateProjectionSyntax($projection) {
        $errors = [];
        $hasInclusion = false;
        $hasExclusion = false;
        
        foreach ($projection as $field => $value) {
            if ($field === '_id') continue;
            
            if ($value === 1) {
                $hasInclusion = true;
            } elseif ($value === 0) {
                $hasExclusion = true;
            } else {
                $errors[] = "Invalid projection value for field '$field': must be 0 or 1";
            }
        }
        
        if ($hasInclusion && $hasExclusion) {
            $errors[] = 'Cannot mix inclusion and exclusion projections (except for _id)';
        }
        
        return $errors;
    }
    
    public function safeProjectionQuery($filter, $projection, $options = []) {
        $validationErrors = $this->validateProjectionSyntax($projection);
        
        if (!empty($validationErrors)) {
            throw new InvalidArgumentException(
                'Invalid projection syntax: ' . implode(', ', $validationErrors)
            );
        }
        
        try {
            return $this->collection->find($filter, array_merge($options, ['projection' => $projection]));
        } catch (MongoDB\Driver\Exception\Exception $e) {
            $this->logProjectionError($filter, $projection, $e);
            throw $e;
        }
    }
    
    private function logProjectionError($filter, $projection, $exception) {
        error_log(sprintf(
            'Projection error with filter %s and projection %s: %s',
            json_encode($filter),
            json_encode($projection),
            $exception->getMessage()
        ));
    }
    
    public function handleMixedProjection($projection) {
        $cleanProjection = [];
        $hasInclusion = false;
        
        foreach ($projection as $field => $value) {
            if ($field === '_id') {
                $cleanProjection[$field] = $value;
                continue;
            }
            
            if ($value === 1) {
                $hasInclusion = true;
                $cleanProjection[$field] = $value;
            }
        }
        
        if (!$hasInclusion) {
            foreach ($projection as $field => $value) {
                if ($field !== '_id' && $value === 0) {
                    $cleanProjection[$field] = $value;
                }
            }
        }
        
        return $cleanProjection;
    }
}

投影性能问题

不当使用投影导致的性能问题:

php
class ProjectionPerformanceHandler {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function analyzeProjectionPerformance($filter, $projection) {
        $command = new MongoDB\Driver\Command([
            'explain' => $this->collection->getNamespace() . '.find',
            'filter' => $filter,
            'projection' => $projection,
            'verbosity' => 'executionStats'
        ]);
        
        $cursor = $this->collection->getManager()->executeCommand(
            $this->collection->getDatabaseName(),
            $command
        );
        
        $result = $cursor->toArray()[0];
        $executionStats = $result['executionStats'];
        
        $analysis = [
            'execution_time_ms' => $executionStats['executionTimeMillis'],
            'docs_examined' => $executionStats['totalDocsExamined'],
            'docs_returned' => $executionStats['totalDocsExamined'],
            'index_used' => $result['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? null,
            'is_covered' => $executionStats['totalDocsExamined'] === 0
        ];
        
        $analysis['efficiency_score'] = $this->calculateEfficiencyScore($analysis);
        
        return $analysis;
    }
    
    private function calculateEfficiencyScore($analysis) {
        if ($analysis['is_covered']) {
            return 100;
        }
        
        $efficiency = 100 - ($analysis['docs_examined'] / max($analysis['docs_returned'], 1) * 10);
        return max(0, min(100, $efficiency));
    }
    
    public function optimizeProjection($filter, $projection, $requirements) {
        $currentAnalysis = $this->analyzeProjectionPerformance($filter, $projection);
        
        $optimizations = [];
        
        if (!$currentAnalysis['is_covered'] && !empty($requirements['required_fields'])) {
            $optimizations[] = $this->createCoveringProjection($filter, $requirements['required_fields']);
        }
        
        if ($currentAnalysis['efficiency_score'] < 50) {
            $optimizations[] = $this->reduceProjectionFields($projection, $requirements);
        }
        
        return $optimizations;
    }
    
    private function createCoveringProjection($filter, $requiredFields) {
        $projection = [];
        foreach ($requiredFields as $field) {
            $projection[$field] = 1;
        }
        
        return [
            'type' => 'covering_index',
            'projection' => $projection,
            'benefit' => 'Create covering index for better performance'
        ];
    }
    
    private function reduceProjectionFields($projection, $requirements) {
        $reducedProjection = [];
        
        foreach ($projection as $field => $value) {
            if (in_array($field, $requirements['required_fields'])) {
                $reducedProjection[$field] = $value;
            }
        }
        
        return [
            'type' => 'field_reduction',
            'projection' => $reducedProjection,
            'benefit' => 'Reduce number of projected fields'
        ];
    }
}

常见应用场景

API响应优化

优化API响应数据大小:

php
class APIResponseOptimizer {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function getOptimizedAPIResponse($filter, $endpointType) {
        $projection = $this->getAPIProjection($endpointType);
        
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'meta' => [
                'endpoint' => $endpointType,
                'fields_returned' => array_keys($projection),
                'timestamp' => new DateTime()
            ]
        ];
    }
    
    private function getAPIProjection($endpointType) {
        $projections = [
            'list' => [
                '_id' => 1,
                'name' => 1,
                'status' => 1,
                'created_at' => 1
            ],
            'detail' => [
                '_id' => 1,
                'name' => 1,
                'description' => 1,
                'status' => 1,
                'created_at' => 1,
                'updated_at' => 1
            ],
            'summary' => [
                '_id' => 1,
                'name' => 1,
                'count' => 1
            ],
            'public' => [
                '_id' => 1,
                'name' => 1,
                'public_info' => 1
            ]
        ];
        
        return $projections[$endpointType] ?? $projections['list'];
    }
    
    public function getPaginatedAPIResponse($filter, $endpointType, $page, $pageSize) {
        $projection = $this->getAPIProjection($endpointType);
        $skip = ($page - 1) * $pageSize;
        
        $options = [
            'projection' => $projection,
            'skip' => $skip,
            'limit' => $pageSize,
            'sort' => ['created_at' => -1]
        ];
        
        $documents = $this->collection->find($filter, $options);
        $totalDocuments = $this->collection->countDocuments($filter);
        
        return [
            'data' => iterator_to_array($documents),
            'pagination' => [
                'current_page' => $page,
                'page_size' => $pageSize,
                'total_documents' => $totalDocuments,
                'total_pages' => ceil($totalDocuments / $pageSize)
            ],
            'meta' => [
                'endpoint' => $endpointType,
                'fields_returned' => array_keys($projection)
            ]
        ];
    }
    
    public function getSecureAPIResponse($filter, $userRole) {
        $projection = $this->getSecureProjection($userRole);
        
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'meta' => [
                'user_role' => $userRole,
                'fields_returned' => array_keys($projection)
            ]
        ];
    }
    
    private function getSecureProjection($userRole) {
        $projections = [
            'admin' => [],
            'user' => [
                'password' => 0,
                'internal_notes' => 0,
                'security_questions' => 0
            ],
            'guest' => [
                'password' => 0,
                'email' => 0,
                'phone' => 0,
                'internal_notes' => 0,
                'security_questions' => 0
            ]
        ];
        
        return $projections[$userRole] ?? $projections['guest'];
    }
}

移动应用数据优化

为移动应用优化数据传输:

php
class MobileDataOptimizer {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function getMobileOptimizedData($filter, $dataType) {
        $projection = $this->getMobileProjection($dataType);
        
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'compression_info' => $this->getCompressionInfo($projection)
        ];
    }
    
    private function getMobileProjection($dataType) {
        $projections = [
            'feed' => [
                '_id' => 1,
                'title' => 1,
                'thumbnail' => 1,
                'created_at' => 1,
                'likes_count' => 1
            ],
            'detail' => [
                '_id' => 1,
                'title' => 1,
                'content' => 1,
                'images' => ['$slice' => 5],
                'created_at' => 1,
                'author' => 1
            ],
            'notification' => [
                '_id' => 1,
                'title' => 1,
                'message' => 1,
                'created_at' => 1,
                'read' => 1
            ],
            'profile' => [
                '_id' => 1,
                'name' => 1,
                'avatar' => 1,
                'bio' => 1
            ]
        ];
        
        return $projections[$dataType] ?? $projections['feed'];
    }
    
    private function getCompressionInfo($projection) {
        $fieldCount = count($projection);
        
        return [
            'fields_returned' => $fieldCount,
            'estimated_size_reduction' => $this->estimateSizeReduction($fieldCount),
            'compression_ratio' => $this->calculateCompressionRatio($fieldCount)
        ];
    }
    
    private function estimateSizeReduction($fieldCount) {
        $averageFields = 15;
        $reduction = (($averageFields - $fieldCount) / $averageFields) * 100;
        return max(0, min(100, $reduction));
    }
    
    private function calculateCompressionRatio($fieldCount) {
        $averageFields = 15;
        return round($averageFields / max($fieldCount, 1), 2);
    }
    
    public function getBandwidthOptimizedData($filter, $bandwidthLevel) {
        $projection = $this->getBandwidthProjection($bandwidthLevel);
        
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'bandwidth_info' => [
                'level' => $bandwidthLevel,
                'fields_returned' => array_keys($projection),
                'estimated_size' => $this->estimateDataSize($projection)
            ]
        ];
    }
    
    private function getBandwidthProjection($bandwidthLevel) {
        $projections = [
            'high' => [
                '_id' => 1,
                'title' => 1,
                'content' => 1,
                'images' => ['$slice' => 10],
                'created_at' => 1
            ],
            'medium' => [
                '_id' => 1,
                'title' => 1,
                'content' => 1,
                'images' => ['$slice' => 3],
                'created_at' => 1
            ],
            'low' => [
                '_id' => 1,
                'title' => 1,
                'thumbnail' => 1,
                'created_at' => 1
            ]
        ];
        
        return $projections[$bandwidthLevel] ?? $projections['medium'];
    }
    
    private function estimateDataSize($projection) {
        $baseSize = 1000;
        $fieldMultiplier = count($projection) * 100;
        return $baseSize + $fieldMultiplier;
    }
}

企业级进阶应用场景

数据安全与隐私保护

使用投影保护敏感数据:

php
class DataSecurityProjection {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function getSecureUserData($filter, $userRole, $context) {
        $projection = $this->buildSecurityProjection($userRole, $context);
        
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'security_context' => [
                'user_role' => $userRole,
                'access_context' => $context,
                'fields_hidden' => $this->getHiddenFields($projection)
            ]
        ];
    }
    
    private function buildSecurityProjection($userRole, $context) {
        $baseProjection = $this->getRoleBasedProjection($userRole);
        $contextProjection = $this->getContextBasedProjection($context);
        
        return array_merge($baseProjection, $contextProjection);
    }
    
    private function getRoleBasedProjection($userRole) {
        $projections = [
            'admin' => [],
            'manager' => [
                'salary' => 0,
                'ssn' => 0,
                'password_hash' => 0
            ],
            'employee' => [
                'salary' => 0,
                'ssn' => 0,
                'password_hash' => 0,
                'performance_review' => 0,
                'manager_notes' => 0
            ],
            'contractor' => [
                'salary' => 0,
                'ssn' => 0,
                'password_hash' => 0,
                'performance_review' => 0,
                'manager_notes' => 0,
                'internal_contact' => 0
            ]
        ];
        
        return $projections[$userRole] ?? $projections['contractor'];
    }
    
    private function getContextBasedProjection($context) {
        $projections = [
            'public' => [
                'email' => 0,
                'phone' => 0,
                'address' => 0
            ],
            'internal' => [],
            'audit' => []
        ];
        
        return $projections[$context] ?? $projections['public'];
    }
    
    private function getHiddenFields($projection) {
        $hiddenFields = [];
        
        foreach ($projection as $field => $value) {
            if ($value === 0) {
                $hiddenFields[] = $field;
            }
        }
        
        return $hiddenFields;
    }
    
    public function getCompliantData($filter, $complianceRequirements) {
        $projection = $this->buildComplianceProjection($complianceRequirements);
        
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'compliance_info' => [
                'requirements' => $complianceRequirements,
                'fields_masked' => $this->getMaskedFields($projection),
                'audit_log' => $this->createAuditLog($complianceRequirements)
            ]
        ];
    }
    
    private function buildComplianceProjection($requirements) {
        $projection = [];
        
        foreach ($requirements as $requirement) {
            switch ($requirement) {
                case 'gdpr':
                    $projection['personal_identifiers'] = 0;
                    $projection['sensitive_personal_data'] = 0;
                    break;
                case 'hipaa':
                    $projection['protected_health_info'] = 0;
                    $projection['medical_records'] = 0;
                    break;
                case 'pci_dss':
                    $projection['credit_card_numbers'] = 0;
                    $projection['cvv_codes'] = 0;
                    break;
            }
        }
        
        return $projection;
    }
    
    private function getMaskedFields($projection) {
        return array_keys(array_filter($projection, function($value) {
            return $value === 0;
        }));
    }
    
    private function createAuditLog($requirements) {
        return [
            'timestamp' => new DateTime(),
            'compliance_standards' => $requirements,
            'data_access_type' => 'projection_masked',
            'user_id' => 'system'
        ];
    }
}

多租户数据隔离

使用投影实现多租户数据隔离:

php
class MultiTenantProjection {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function getTenantData($filter, $tenantId, $accessLevel) {
        $tenantFilter = $this->addTenantFilter($filter, $tenantId);
        $projection = $this->getTenantProjection($accessLevel);
        
        $documents = $this->collection->find($tenantFilter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'tenant_context' => [
                'tenant_id' => $tenantId,
                'access_level' => $accessLevel,
                'data_isolation' => 'enabled'
            ]
        ];
    }
    
    private function addTenantFilter($filter, $tenantId) {
        $tenantFilter = ['tenant_id' => $tenantId];
        
        if (empty($filter)) {
            return $tenantFilter;
        }
        
        return ['$and' => [$tenantFilter, $filter]];
    }
    
    private function getTenantProjection($accessLevel) {
        $projections = [
            'full' => [],
            'restricted' => [
                'internal_settings' => 0,
                'api_keys' => 0,
                'webhooks' => 0
            ],
            'minimal' => [
                'internal_settings' => 0,
                'api_keys' => 0,
                'webhooks' => 0,
                'billing_info' => 0,
                'usage_stats' => 0
            ]
        ];
        
        return $projections[$accessLevel] ?? $projections['minimal'];
    }
    
    public function getSharedData($filter, $tenantId, $sharedResourceType) {
        $sharedFilter = $this->buildSharedResourceFilter($filter, $tenantId, $sharedResourceType);
        $projection = $this->getSharedResourceProjection($sharedResourceType);
        
        $documents = $this->collection->find($sharedFilter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'sharing_context' => [
                'tenant_id' => $tenantId,
                'resource_type' => $sharedResourceType,
                'access_mode' => 'read_only'
            ]
        ];
    }
    
    private function buildSharedResourceFilter($filter, $tenantId, $resourceType) {
        $baseFilter = [
            'shared_with' => $tenantId,
            'resource_type' => $resourceType,
            'access_level' => ['$gte' => 'read']
        ];
        
        if (empty($filter)) {
            return $baseFilter;
        }
        
        return ['$and' => [$baseFilter, $filter]];
    }
    
    private function getSharedResourceProjection($resourceType) {
        $projections = [
            'document' => [
                'owner_id' => 0,
                'sharing_settings' => 0,
                'access_logs' => 0
            ],
            'report' => [
                'owner_id' => 0,
                'raw_data' => 0,
                'query_details' => 0
            ],
            'dashboard' => [
                'owner_id' => 0,
                'configuration' => 0,
                'access_tokens' => 0
            ]
        ];
        
        return $projections[$resourceType] ?? [];
    }
}

行业最佳实践

投影设计原则

设计高效投影的原则:

php
class ProjectionDesignPrinciples {
    public function designOptimalProjection($requirements) {
        $projection = [];
        
        foreach ($requirements['required_fields'] as $field) {
            $projection[$field] = 1;
        }
        
        if (!empty($requirements['exclude_fields'])) {
            foreach ($requirements['exclude_fields'] as $field) {
                $projection[$field] = 0;
            }
        }
        
        $projection = $this->applyPerformanceOptimizations($projection, $requirements);
        
        return $projection;
    }
    
    private function applyPerformanceOptimizations($projection, $requirements) {
        $optimized = $projection;
        
        if (!empty($requirements['performance_priority'])) {
            $optimized = $this->prioritizeFields($optimized, $requirements['performance_priority']);
        }
        
        if (!empty($requirements['size_constraints'])) {
            $optimized = $this->applySizeConstraints($optimized, $requirements['size_constraints']);
        }
        
        return $optimized;
    }
    
    private function prioritizeFields($projection, $priority) {
        $prioritized = [];
        
        foreach ($priority as $field) {
            if (isset($projection[$field])) {
                $prioritized[$field] = $projection[$field];
            }
        }
        
        foreach ($projection as $field => $value) {
            if (!isset($prioritized[$field])) {
                $prioritized[$field] = $value;
            }
        }
        
        return $prioritized;
    }
    
    private function applySizeConstraints($projection, $constraints) {
        $constrained = $projection;
        
        if ($constraints['max_fields'] && count($projection) > $constraints['max_fields']) {
            $constrained = array_slice($projection, 0, $constraints['max_fields'], true);
        }
        
        return $constrained;
    }
    
    public function validateProjectionDesign($projection, $requirements) {
        $validation = [
            'is_valid' => true,
            'issues' => [],
            'recommendations' => []
        ];
        
        $missingFields = array_diff($requirements['required_fields'], array_keys($projection));
        if (!empty($missingFields)) {
            $validation['is_valid'] = false;
            $validation['issues'][] = 'Missing required fields: ' . implode(', ', $missingFields);
        }
        
        $excludedFields = array_filter($projection, function($value) {
            return $value === 0;
        });
        
        if (count($excludedFields) > count($projection) / 2) {
            $validation['recommendations'][] = 'Consider using inclusion projection instead of exclusion';
        }
        
        if (count($projection) > 20) {
            $validation['recommendations'][] = 'Large number of projected fields, consider reducing';
        }
        
        return $validation;
    }
}

投影性能监控

监控投影操作的性能:

php
class ProjectionPerformanceMonitor {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function monitorProjectionPerformance($filter, $projection) {
        $startTime = microtime(true);
        
        try {
            $documents = $this->collection->find($filter, ['projection' => $projection]);
            $documentArray = iterator_to_array($documents);
            
            $executionTime = (microtime(true) - $startTime) * 1000;
            
            $metrics = $this->collectProjectionMetrics($filter, $projection, $executionTime, $documentArray);
            
            $this->logProjectionMetrics($metrics);
            
            return [
                'data' => $documentArray,
                'metrics' => $metrics
            ];
        } catch (Exception $e) {
            $executionTime = (microtime(true) - $startTime) * 1000;
            $this->logProjectionError($filter, $projection, $e, $executionTime);
            throw $e;
        }
    }
    
    private function collectProjectionMetrics($filter, $projection, $executionTime, $documents) {
        $totalSize = 0;
        foreach ($documents as $document) {
            $totalSize += strlen(json_encode($document));
        }
        
        return [
            'execution_time_ms' => $executionTime,
            'documents_returned' => count($documents),
            'total_size_bytes' => $totalSize,
            'avg_size_bytes' => $totalSize / max(count($documents), 1),
            'fields_projected' => count($projection),
            'projection_type' => $this->determineProjectionType($projection)
        ];
    }
    
    private function determineProjectionType($projection) {
        $hasInclusion = false;
        $hasExclusion = false;
        
        foreach ($projection as $field => $value) {
            if ($field === '_id') continue;
            
            if ($value === 1) {
                $hasInclusion = true;
            } elseif ($value === 0) {
                $hasExclusion = true;
            }
        }
        
        if ($hasInclusion && $hasExclusion) {
            return 'mixed';
        } elseif ($hasInclusion) {
            return 'inclusion';
        } elseif ($hasExclusion) {
            return 'exclusion';
        }
        
        return 'none';
    }
    
    private function logProjectionMetrics($metrics) {
        $logEntry = [
            'timestamp' => new DateTime(),
            'collection' => $this->collection->getNamespace(),
            'metrics' => $metrics
        ];
        
        error_log('Projection metrics: ' . json_encode($logEntry));
    }
    
    private function logProjectionError($filter, $projection, $exception, $executionTime) {
        $logEntry = [
            'timestamp' => new DateTime(),
            'collection' => $this->collection->getNamespace(),
            'filter' => $filter,
            'projection' => $projection,
            'error' => $exception->getMessage(),
            'execution_time_ms' => $executionTime
        ];
        
        error_log('Projection error: ' . json_encode($logEntry));
    }
    
    public function analyzeProjectionTrends($timeRange = '24h') {
        $trends = [
            'avg_execution_time' => 0,
            'avg_document_size' => 0,
            'most_common_projections' => [],
            'performance_issues' => []
        ];
        
        return $trends;
    }
}

常见问题答疑

Q1: 如何选择包含还是排除投影?

选择投影类型的决策标准:

php
class ProjectionTypeSelector {
    public function selectProjectionType($requirements) {
        $totalFields = $requirements['total_fields'] ?? 0;
        $requiredFields = $requirements['required_fields'] ?? [];
        $sensitiveFields = $requirements['sensitive_fields'] ?? [];
        
        if (count($requiredFields) < $totalFields / 2) {
            return [
                'type' => 'inclusion',
                'reason' => 'Fewer fields to include than exclude',
                'projection' => $this->buildInclusionProjection($requiredFields)
            ];
        }
        
        if (!empty($sensitiveFields)) {
            return [
                'type' => 'exclusion',
                'reason' => 'Exclude sensitive fields',
                'projection' => $this->buildExclusionProjection($sensitiveFields)
            ];
        }
        
        return [
            'type' => 'inclusion',
            'reason' => 'Default to inclusion for clarity',
            'projection' => $this->buildInclusionProjection($requiredFields)
        ];
    }
    
    private function buildInclusionProjection($fields) {
        $projection = [];
        foreach ($fields as $field) {
            $projection[$field] = 1;
        }
        return $projection;
    }
    
    private function buildExclusionProjection($fields) {
        $projection = [];
        foreach ($fields as $field) {
            $projection[$field] = 0;
        }
        return $projection;
    }
}

Q2: 如何优化投影以提高性能?

优化投影性能的方法:

php
class ProjectionPerformanceOptimizer {
    public function optimizeProjection($projection, $context) {
        $optimizations = [];
        
        if ($context['data_size'] === 'large') {
            $optimizations[] = $this->reduceFieldCount($projection);
        }
        
        if ($context['network_speed'] === 'slow') {
            $optimizations[] = $this->minimizeDataTransfer($projection);
        }
        
        if ($context['memory_constraint'] === 'high') {
            $optimizations[] = $this->optimizeMemoryUsage($projection);
        }
        
        return $optimizations;
    }
    
    private function reduceFieldCount($projection) {
        return [
            'optimization' => 'reduce_field_count',
            'action' => 'Limit to essential fields only',
            'benefit' => 'Reduces data transfer and processing time'
        ];
    }
    
    private function minimizeDataTransfer($projection) {
        return [
            'optimization' => 'minimize_data_transfer',
            'action' => 'Use array slicing and field exclusion',
            'benefit' => 'Reduces network bandwidth usage'
        ];
    }
    
    private function optimizeMemoryUsage($projection) {
        return [
            'optimization' => 'optimize_memory_usage',
            'action' => 'Use covered queries with proper indexes',
            'benefit' => 'Reduces server memory consumption'
        ];
    }
}

Q3: 如何处理嵌套字段的投影?

处理嵌套字段投影的方法:

php
class NestedFieldProjection {
    public function projectNestedFields($nestedStructure) {
        $projection = [];
        
        foreach ($nestedStructure as $field => $config) {
            if ($config['include']) {
                $projection[$field] = 1;
                
                if (!empty($config['nested_fields'])) {
                    foreach ($config['nested_fields'] as $nestedField) {
                        $projection["$field.$nestedField"] = 1;
                    }
                }
            }
        }
        
        return $projection;
    }
    
    public function projectArrayElements($arrayField, $config) {
        $projection = [];
        
        if ($config['slice']) {
            $projection[$arrayField] = ['$slice' => $config['slice']];
        }
        
        if ($config['elemMatch']) {
            $projection[$arrayField] = ['$elemMatch' => $config['elemMatch']];
        }
        
        return $projection;
    }
}

实战练习

练习1: 实现API响应优化

实现API响应的投影优化:

php
class APIProjectionOptimizer {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function getOptimizedResponse($request) {
        $filter = $this->buildFilter($request);
        $projection = $this->buildProjection($request);
        
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'meta' => [
                'fields_returned' => array_keys($projection),
                'optimization_level' => $request['optimization_level'] ?? 'standard'
            ]
        ];
    }
    
    private function buildFilter($request) {
        $filter = [];
        
        if (!empty($request['filter'])) {
            $filter = $request['filter'];
        }
        
        return $filter;
    }
    
    private function buildProjection($request) {
        $endpoint = $request['endpoint'] ?? 'default';
        $optimizationLevel = $request['optimization_level'] ?? 'standard';
        
        $baseProjection = $this->getEndpointProjection($endpoint);
        $optimizedProjection = $this->applyOptimization($baseProjection, $optimizationLevel);
        
        return $optimizedProjection;
    }
    
    private function getEndpointProjection($endpoint) {
        $projections = [
            'list' => [
                '_id' => 1,
                'name' => 1,
                'status' => 1,
                'created_at' => 1
            ],
            'detail' => [
                '_id' => 1,
                'name' => 1,
                'description' => 1,
                'status' => 1,
                'created_at' => 1,
                'updated_at' => 1
            ],
            'summary' => [
                '_id' => 1,
                'name' => 1,
                'count' => 1
            ]
        ];
        
        return $projections[$endpoint] ?? $projections['list'];
    }
    
    private function applyOptimization($projection, $level) {
        switch ($level) {
            case 'minimal':
                return array_intersect_key($projection, array_flip(['_id', 'name']));
            case 'aggressive':
                return array_slice($projection, 0, 3, true);
            default:
                return $projection;
        }
    }
}

练习2: 实现数据安全投影

实现数据安全的投影:

php
class SecurityProjection {
    private $collection;
    
    public function __construct($collection) {
        $this->collection = $collection;
    }
    
    public function getSecureData($request) {
        $filter = $this->buildFilter($request);
        $projection = $this->buildSecurityProjection($request);
        
        $documents = $this->collection->find($filter, ['projection' => $projection]);
        
        return [
            'data' => iterator_to_array($documents),
            'security_info' => [
                'user_role' => $request['user_role'],
                'access_context' => $request['access_context'],
                'fields_hidden' => $this->getHiddenFields($projection)
            ]
        ];
    }
    
    private function buildFilter($request) {
        $filter = $request['filter'] ?? [];
        
        if (!empty($request['tenant_id'])) {
            $filter['tenant_id'] = $request['tenant_id'];
        }
        
        return $filter;
    }
    
    private function buildSecurityProjection($request) {
        $roleProjection = $this->getRoleProjection($request['user_role']);
        $contextProjection = $this->getContextProjection($request['access_context']);
        
        return array_merge($roleProjection, $contextProjection);
    }
    
    private function getRoleProjection($userRole) {
        $projections = [
            'admin' => [],
            'user' => [
                'password' => 0,
                'internal_notes' => 0
            ],
            'guest' => [
                'password' => 0,
                'email' => 0,
                'internal_notes' => 0
            ]
        ];
        
        return $projections[$userRole] ?? $projections['guest'];
    }
    
    private function getContextProjection($context) {
        $projections = [
            'public' => [
                'email' => 0,
                'phone' => 0
            ],
            'internal' => []
        ];
        
        return $projections[$context] ?? $projections['public'];
    }
    
    private function getHiddenFields($projection) {
        return array_keys(array_filter($projection, function($value) {
            return $value === 0;
        }));
    }
}

知识点总结

核心概念

  1. 投影类型

    • 包含投影(inclusion)
    • 排除投影(exclusion)
    • 混合投影(mixed)
    • 聚合投影(aggregation)
  2. 投影操作

    • 字段包含和排除
    • 数组切片($slice)
    • 元素匹配($elemMatch)
    • 计算字段投影
  3. 性能优化

    • 索引覆盖查询
    • 减少数据传输
    • 优化内存使用
    • 监控投影性能

最佳实践

  1. 投影设计

    • 优先使用包含投影
    • 避免混合投影
    • 合理选择投影字段
    • 考虑数据安全
  2. 性能优化

    • 创建覆盖索引
    • 使用数组切片
    • 限制返回字段数量
    • 监控查询性能
  3. 安全考虑

    • 排除敏感字段
    • 基于角色的投影
    • 合规性要求
    • 审计日志

拓展参考资料

  • MongoDB官方文档:投影操作
  • MongoDB官方文档:聚合投影
  • PHP MongoDB驱动文档:查询投影
  • 数据安全与隐私保护最佳实践
  • MongoDB性能优化指南