• 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

72.73
/src/Relationship/OneHasMany.php
1
<?php
2

3
namespace Anorm\Relationship;
4

5
use Anorm\DataMapper;
6
use Anorm\Relationship\BatchLoader\OneHasManyBatchLoader;
7
use Anorm\Relationship\Strategy\QueryStrategySelector;
8
use Anorm\Relationship\Strategy\QueryStrategyInterface;
9
use Anorm\Relationship\Strategy\JoinWithSelectionLoader;
10

11
/**
12
 * One-to-Many relationship
13
 * This model has many instances of another model
14
 */
15
class OneHasMany extends Relationship
16
{
17
    /**
18
     * Get the relationship type
19
     */
20
    public function getType()
21
    {
22
        return 'oneHasMany';
5✔
23
    }
24

25
    /**
26
     * Load the related models for a one-to-many relationship
27
     *
28
     * @param object $sourceModel The model instance that owns the relationship
29
     * @param \PDO $pdo The database connection
30
     * @return array Array of related model instances
31
     */
32
    public function load($sourceModel, \PDO $pdo)
33
    {
34
        $relatedClass = $this->relatedModelClass;
27✔
35
        $sourceValue = $sourceModel->{$this->primaryKey};
27✔
36

37
        if ($sourceValue === null) {
27✔
38
            return [];
×
39
        }
40

41
        // Create an instance of the related model to get its mapper
42
        $relatedInstance = new $relatedClass($pdo);
27✔
43
        $mapper = $relatedInstance->_mapper;
27✔
44

45
        // Build the query to find related models
46
        // The foreign key should be a database column name, not a property name
47
        $sql = "SELECT * FROM `{$mapper->table}` WHERE `{$this->foreignKey}` = ?";
27✔
48

49
        $result = $mapper->query($sql, [$sourceValue]);
27✔
50
        $relatedModels = [];
27✔
51

52
        while ($data = $result->fetch(\PDO::FETCH_ASSOC)) {
27✔
53
            $relatedModel = new $relatedClass($pdo);
26✔
54
            $relatedModel->_mapper->readArray($relatedModel, $data);
26✔
55
            $relatedModels[] = $relatedModel;
26✔
56
        }
57

58
        return $relatedModels;
27✔
59
    }
60

61
    /**
62
     * Generate JOIN clause for one-to-many relationship
63
     *
64
     * @param string $sourceTable The source table name
65
     * @param string $relatedTable The related table name
66
     * @return string The JOIN clause
67
     */
68
    public function generateJoinClause($sourceTable, $relatedTable)
69
    {
70
        return "LEFT JOIN `{$relatedTable}` ON `{$sourceTable}`.`{$this->primaryKey}` = `{$relatedTable}`.`{$this->foreignKey}`";
2✔
71
    }
72

73
    /**
74
     * Generate foreign key constraint SQL for OneHasMany relationship
75
     * Creates foreign key on the related table pointing back to source table
76
     */
77
    public function generateForeignKeyConstraints($sourceTable)
78
    {
79
        $targetTable = $this->getTableNameFromModelClass($this->relatedModelClass);
×
80
        $constraintName = $this->getConstraintName($targetTable, $sourceTable);
×
81
        $onDelete = $this->constraintOptions['on_delete'];
×
82
        $onUpdate = $this->constraintOptions['on_update'];
×
83

84
        $sql = "ALTER TABLE `{$targetTable}`
×
85
                ADD CONSTRAINT `{$constraintName}`
×
86
                FOREIGN KEY (`{$this->foreignKey}`)
×
87
                REFERENCES `{$sourceTable}`(`{$this->primaryKey}`)
×
88
                ON DELETE {$onDelete}
×
89
                ON UPDATE {$onUpdate}";
×
90

91
        return [$sql];
×
92
    }
93

94
    /**
95
     * Load relationships for multiple source models in a single batch operation
96
     *
97
     * @param array $sourceModels Array of model instances that need relationships loaded
98
     * @param \PDO $pdo Database connection
99
     * @param array|null $fieldSelection Optional field selection for optimization
100
     * @return array Associative array of loaded relationship data, keyed by source model identifier
101
     */
102
    public function batchLoad(array $sourceModels, \PDO $pdo, ?array $fieldSelection = null): array
103
    {
104
        // Select optimal strategy based on data characteristics
105
        $strategySelector = new QueryStrategySelector();
11✔
106
        $strategy = $strategySelector->selectStrategy($this, count($sourceModels), $fieldSelection);
11✔
107

108
        switch ($strategy) {
109
            case QueryStrategyInterface::STRATEGY_JOIN_WITH_SELECTION:
11✔
NEW
110
                $loader = new JoinWithSelectionLoader();
×
NEW
111
                $relationshipSpec = $fieldSelection ? $this->propertyName . ':' . implode(',', $fieldSelection) : $this->propertyName;
×
NEW
112
                return $loader->batchLoad($sourceModels, $relationshipSpec);
×
113

114
            case QueryStrategyInterface::STRATEGY_INDIVIDUAL_LOADING:
11✔
115
                return $this->loadIndividually($sourceModels, $pdo);
6✔
116

117
            case QueryStrategyInterface::STRATEGY_IN_CLAUSE_BATCH:
5✔
118
            default:
119
                $batchLoader = new OneHasManyBatchLoader();
5✔
120
                return $batchLoader->batchLoad($sourceModels, $this->propertyName, $fieldSelection);
5✔
121
        }
122
    }
123

124
    /**
125
     * Load relationships individually for each source model
126
     *
127
     * @param array $sourceModels Array of model instances
128
     * @param \PDO $pdo Database connection
129
     * @return array Associative array of loaded data
130
     */
131
    private function loadIndividually(array $sourceModels, \PDO $pdo): array
132
    {
133
        $results = [];
6✔
134

135
        foreach ($sourceModels as $model) {
6✔
136
            $primaryKeyValue = $model->{$this->primaryKey};
5✔
137
            if ($primaryKeyValue !== null) {
5✔
138
                // Load related models for this specific source model
139
                $relatedModels = DataMapper::find($this->relatedModelClass, $pdo)
5✔
140
                    ->where($this->foreignKey . ' = ?', [$primaryKeyValue])
5✔
141
                    ->some();
5✔
142

143
                $results[$primaryKeyValue] = iterator_to_array($relatedModels);
5✔
144
            }
145
        }
146

147
        return $results;
6✔
148
    }
149

150
    /**
151
     * Distribute batch-loaded results to their corresponding source models
152
     *
153
     * @param array $sourceModels Array of model instances to receive the loaded data
154
     * @param array $batchResults Results from batchLoad(), keyed by source model identifier
155
     * @return void
156
     */
157
    public function distributeBatchResults(array $sourceModels, array $batchResults): void
158
    {
159
        $batchLoader = new OneHasManyBatchLoader();
11✔
160
        $batchLoader->distributeBatchResults($sourceModels, $batchResults, $this->propertyName);
11✔
161
    }
162

163
    /**
164
     * Estimate the data size for this relationship with given parameters
165
     *
166
     * @param int $sourceCount Number of source models
167
     * @param array|null $fieldSelection Specific fields to load, or null for all fields
168
     * @return int Estimated data size in bytes
169
     */
170
    public function estimateDataSize(int $sourceCount, ?array $fieldSelection = null): int
171
    {
172
        // Estimate based on one-to-many cardinality
173
        $avgRelatedRecords = 5; // Conservative estimate for hasMany
3✔
174
        $avgRecordSize = 1024; // 1KB per record estimate
3✔
175

176
        if ($fieldSelection !== null && !empty($fieldSelection)) {
3✔
177
            // Estimate size for selected fields only
178
            $avgRecordSize = count($fieldSelection) * 50; // 50 bytes per field estimate
1✔
179
        }
180

181
        return (int) ($sourceCount * $avgRelatedRecords * $avgRecordSize);
3✔
182
    }
183

184
    /**
185
     * Get the cardinality type of this relationship
186
     *
187
     * @return string One of: 'one-to-one', 'one-to-many', 'many-to-one', 'many-to-many'
188
     */
189
    public function getCardinality(): string
190
    {
191
        return 'one-to-many';
3✔
192
    }
193
}
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