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

saygoweb / anorm / 18483557340

14 Oct 2025 02:25AM UTC coverage: 84.208% (+3.7%) from 80.483%
18483557340

Pull #42

github

web-flow
Merge 3a2243fbd 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

97.83
/src/Relationship/BatchLoader/OneHasManyBatchLoader.php
1
<?php
2

3
namespace Anorm\Relationship\BatchLoader;
4

5
/**
6
 * Batch loader for One-to-Many relationships
7
 *
8
 * Optimizes loading of hasMany relationships by collecting all foreign keys
9
 * and executing a single IN clause query instead of N individual queries.
10
 */
11
class OneHasManyBatchLoader implements BatchLoaderInterface
12
{
13
    /**
14
     * Load one-to-many 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 source model primary key
20
     */
21
    public function batchLoad(array $sourceModels, string $relationshipName, ?array $fieldSelection = null): array
22
    {
23
        if (empty($sourceModels)) {
8✔
24
            return [];
1✔
25
        }
26

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

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

36
        // Collect all primary key values from source models
37
        $primaryKeys = [];
7✔
38
        foreach ($sourceModels as $model) {
7✔
39
            $primaryKeyValue = $model->{$relationship->getPrimaryKey()};
7✔
40
            if ($primaryKeyValue !== null) {
7✔
41
                $primaryKeys[] = $primaryKeyValue;
6✔
42
            }
43
        }
44

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

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

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

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

60
        // Handle field selection (basic implementation for now)
61
        $selectClause = '*';
6✔
62
        if ($fieldSelection && !empty($fieldSelection)) {
6✔
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->getForeignKey()}` IN ({$placeholders})";
6✔
69

70
        // Execute the batch query
71
        $result = $mapper->query($sql, $primaryKeys);
6✔
72

73
        // Group results by foreign key value
74
        $groupedResults = [];
6✔
75
        while ($data = $result->fetch(\PDO::FETCH_ASSOC)) {
6✔
76
            $foreignKeyValue = $data[$relationship->getForeignKey()];
6✔
77

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

82
            // Group by foreign key (which corresponds to source model primary key)
83
            if (!isset($groupedResults[$foreignKeyValue])) {
6✔
84
                $groupedResults[$foreignKeyValue] = [];
6✔
85
            }
86
            $groupedResults[$foreignKeyValue][] = $relatedModel;
6✔
87
        }
88

89
        return $groupedResults;
6✔
90
    }
91

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

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

111
        foreach ($sourceModels as $model) {
13✔
112
            $primaryKeyValue = $model->{$relationship->getPrimaryKey()};
13✔
113

114
            // Assign the related models array to the model property
115
            if (isset($batchResults[$primaryKeyValue])) {
13✔
116
                $model->{$relationshipName} = $batchResults[$primaryKeyValue];
11✔
117
            } else {
118
                // No related models found - assign empty array
119
                $model->{$relationshipName} = [];
7✔
120
            }
121
        }
122
    }
123

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

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

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