• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

saygoweb / anorm / 18483020557

14 Oct 2025 01:51AM UTC coverage: 84.208% (+3.7%) from 80.483%
18483020557

Pull #42

github

web-flow
Merge 081631070 into 025025bcf
Pull Request #42: Implement comprehensive join optimization system to solve N+1 query problem

845 of 960 new or added lines in 19 files covered. (88.02%)

66 existing lines in 3 files now uncovered.

1637 of 1944 relevant lines covered (84.21%)

15.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.22
/src/Relationship/BatchLoader/ManyHasOneBatchLoader.php
1
<?php
2

3
namespace Anorm\Relationship\BatchLoader;
4

5
/**
6
 * Batch loader for Many-to-One relationships (belongsTo)
7
 *
8
 * Optimizes loading of belongsTo relationships by collecting all foreign keys
9
 * and executing a single IN clause query instead of N individual queries.
10
 */
11
class ManyHasOneBatchLoader implements BatchLoaderInterface
12
{
13
    /**
14
     * Load many-to-one relationships for multiple source models in a single batch
15
     *
16
     * @param array $sourceModels Array of model instances that need relationships loaded
17
     * @param string $relationshipName Name of the relationship to load
18
     * @param array|null $fieldSelection Optional field selection for optimization
19
     * @return array Associative array of loaded relationship data, keyed by related model primary key
20
     */
21
    public function batchLoad(array $sourceModels, string $relationshipName, ?array $fieldSelection = null): array
22
    {
23
        if (empty($sourceModels)) {
7✔
24
            return [];
1✔
25
        }
26

27
        // Get relationship definition from the first model
28
        $firstModel = reset($sourceModels);
6✔
29
        $relationshipManager = $firstModel->getRelationshipManager();
6✔
30
        $relationship = $relationshipManager->getRelationship($relationshipName);
6✔
31

32
        if (!$relationship) {
6✔
NEW
33
            throw new \Exception("Relationship '{$relationshipName}' not defined");
×
34
        }
35

36
        // Collect all foreign key values from source models
37
        $foreignKeys = [];
6✔
38
        foreach ($sourceModels as $model) {
6✔
39
            $foreignKeyValue = $model->{$relationship->getForeignKey()};
6✔
40
            if ($foreignKeyValue !== null) {
6✔
41
                $foreignKeys[] = $foreignKeyValue;
5✔
42
            }
43
        }
44

45
        if (empty($foreignKeys)) {
6✔
46
            return [];
1✔
47
        }
48

49
        // Remove duplicates and prepare for IN clause
50
        $foreignKeys = array_values(array_unique($foreignKeys));
5✔
51

52
        // Create an instance of the related model to get its mapper
53
        $relatedClass = $relationship->getRelatedModelClass();
5✔
54
        $relatedInstance = new $relatedClass($firstModel->getPdo());
5✔
55
        $mapper = $relatedInstance->_mapper;
5✔
56

57
        // Build the batch query using IN clause
58
        $placeholders = str_repeat('?,', count($foreignKeys) - 1) . '?';
5✔
59

60
        // Handle field selection (basic implementation for now)
61
        $selectClause = '*';
5✔
62
        if ($fieldSelection && !empty($fieldSelection)) {
5✔
63
            // For now, still select all fields to avoid parameter binding issues
64
            // Field selection optimization will be implemented in Phase 3
65
            $selectClause = '*';
1✔
66
        }
67

68
        $sql = "SELECT {$selectClause} FROM `{$mapper->table}` WHERE `{$relationship->getPrimaryKey()}` IN ({$placeholders})";
5✔
69

70

71

72
        // Execute the batch query
73
        $result = $mapper->query($sql, $foreignKeys);
5✔
74

75
        // Create lookup map by primary key value
76
        $lookupMap = [];
5✔
77
        while ($data = $result->fetch(\PDO::FETCH_ASSOC)) {
5✔
78
            $primaryKeyValue = $data[$relationship->getPrimaryKey()];
5✔
79

80
            // Create related model instance
81
            $relatedModel = new $relatedClass($firstModel->getPdo());
5✔
82
            $relatedModel->_mapper->readArray($relatedModel, $data);
5✔
83

84
            // Store in lookup map by primary key
85
            $lookupMap[$primaryKeyValue] = $relatedModel;
5✔
86
        }
87

88
        return $lookupMap;
5✔
89
    }
90

91
    /**
92
     * Distribute batch-loaded results to their corresponding source models
93
     *
94
     * @param array $sourceModels Array of model instances to receive the loaded data
95
     * @param array $batchResults Results from batchLoad(), keyed by related model primary key
96
     * @param string $relationshipName Name of the relationship being distributed
97
     * @return void
98
     */
99
    public function distributeBatchResults(array $sourceModels, array $batchResults, string $relationshipName): void
100
    {
101
        // Get relationship definition to access foreign key
102
        $firstModel = reset($sourceModels);
7✔
103
        $relationshipManager = $firstModel->getRelationshipManager();
7✔
104
        $relationship = $relationshipManager->getRelationship($relationshipName);
7✔
105

106
        if (!$relationship) {
7✔
107
            return; // Relationship not found, skip distribution
1✔
108
        }
109

110
        foreach ($sourceModels as $model) {
6✔
111
            $foreignKeyValue = $model->{$relationship->getForeignKey()};
6✔
112

113
            // Assign the related model to the model property
114
            if ($foreignKeyValue !== null && isset($batchResults[$foreignKeyValue])) {
6✔
115
                $model->{$relationshipName} = $batchResults[$foreignKeyValue];
5✔
116
            } else {
117
                // No related model found - assign null
118
                $model->{$relationshipName} = null;
1✔
119
            }
120
        }
121
    }
122

123
    /**
124
     * Estimate the number of queries this batch loader would execute
125
     *
126
     * @param int $sourceCount Number of source models
127
     * @return int Number of queries (always 1 for batch loading)
128
     */
129
    public function estimateQueryCount(int $sourceCount): int
130
    {
131
        return $sourceCount > 0 ? 1 : 0;
1✔
132
    }
133

134
    /**
135
     * Check if this batch loader can handle the given relationship
136
     *
137
     * @param object $relationship The relationship to check
138
     * @return bool True if this loader can handle the relationship
139
     */
140
    public function canHandle($relationship): bool
141
    {
142
        return $relationship->getType() === 'manyHasOne';
1✔
143
    }
144

145
    /**
146
     * Get the maximum recommended batch size for this loader
147
     *
148
     * @return int Maximum number of source models to process in one batch
149
     */
150
    public function getMaxBatchSize(): int
151
    {
152
        // Conservative limit to avoid hitting database IN clause limits
153
        return 1000;
1✔
154
    }
155

156
    /**
157
     * Get statistics about the batch loading operation
158
     *
159
     * @param array $sourceModels Source models that were processed
160
     * @param array $batchResults Results that were loaded
161
     * @return array Statistics about the operation
162
     */
163
    public function getBatchStatistics(array $sourceModels, array $batchResults): array
164
    {
165
        $uniqueForeignKeys = [];
1✔
166
        foreach ($sourceModels as $model) {
1✔
167
            $firstModel = reset($sourceModels);
1✔
168
            $relationshipManager = $firstModel->getRelationshipManager();
1✔
169
            $relationship = $relationshipManager->getRelationship(''); // This would need the relationship name
1✔
170

171
            if ($relationship) {
1✔
NEW
172
                $foreignKeyValue = $model->{$relationship->getForeignKey()};
×
NEW
173
                if ($foreignKeyValue !== null) {
×
NEW
174
                    $uniqueForeignKeys[$foreignKeyValue] = true;
×
175
                }
176
            }
177
        }
178

179
        return [
1✔
180
            'source_models' => count($sourceModels),
1✔
181
            'unique_foreign_keys' => count($uniqueForeignKeys),
1✔
182
            'loaded_models' => count($batchResults),
1✔
183
            'cache_hit_ratio' => count($uniqueForeignKeys) > 0 ? count($batchResults) / count($uniqueForeignKeys) : 0,
1✔
184
        ];
1✔
185
    }
186
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc