Skip to content

Commit ff1461c

Browse files
committed
feat: bulkUpdate
1 parent e557cc8 commit ff1461c

2 files changed

Lines changed: 147 additions & 0 deletions

File tree

src/Builder/BaseBuilder.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,107 @@ public function update(array|object $data = [])
645645
});
646646
}
647647

648+
/**
649+
* Met à jour plusieurs enregistrements en une seule requête
650+
*
651+
* @param list<array|object> $data Tableau de données à mettre à jour, où chaque élément est un tableau associatif
652+
* @param array|Expression|string $constraints Colonne utilisée pour identifier les enregistrements à mettre à jour (par défaut 'id')
653+
* @param int $chunkSize Taille des lots pour le traitement
654+
*
655+
* @return int|string Nombre de lignes affectées ou la requête SQL en mode test
656+
*
657+
* @throws BadMethodCallException
658+
* @throws InvalidArgumentException
659+
*
660+
* @example
661+
* // Mise à jour simple
662+
* $builder->bulkUpdate([
663+
* ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'],
664+
* ['id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com'],
665+
* ]);
666+
*
667+
* // Mise à jour avec colonne personnalisée
668+
* $builder->bulkUpdate([
669+
* ['code' => 'ABC', 'price' => 100],
670+
* ['code' => 'DEF', 'price' => 150],
671+
* ], 'code');
672+
*/
673+
public function bulkUpdate(array $data, string $column = 'id', int $chunkSize = 100): int|string
674+
{
675+
if ($data === []) {
676+
return 0;
677+
}
678+
679+
// Vérifier la structure des données
680+
$firstRow = reset($data);
681+
if (!is_array($firstRow)) {
682+
throw new InvalidArgumentException('Each row must be an associative array.');
683+
}
684+
685+
// Vérifier que la colonne d'identification existe dans chaque ligne
686+
foreach ($data as $index => $row) {
687+
if (!array_key_exists($column, $row)) {
688+
throw new InvalidArgumentException(
689+
"Column '{$column}' not found in row at index {$index}. Each row must contain the identifier column."
690+
);
691+
}
692+
}
693+
694+
// Extraire les colonnes à mettre à jour (toutes sauf la colonne d'identification)
695+
$updateColumns = array_diff(array_keys($firstRow), [$column]);
696+
697+
if (empty($updateColumns)) {
698+
return 0; // Rien à mettre à jour
699+
}
700+
701+
// Traitement par lots
702+
$totalAffected = 0;
703+
$chunks = array_chunk($data, $chunkSize);
704+
$allSql = [];
705+
706+
$callback = function() use ($chunks, $column, $updateColumns, &$totalAffected, &$allSql) {
707+
foreach ($chunks as $chunk) {
708+
$sql = $this->compiler->compileBulkUpdate($this, $chunk, $column, $updateColumns);
709+
710+
if ($this->testMode) {
711+
$allSql[] = $sql;
712+
} else {
713+
$bindings = $this->buildBulkUpdateBindings($chunk, $column, $updateColumns);
714+
$totalAffected += $this->db->affectingStatement($sql, $bindings);
715+
}
716+
}
717+
718+
return [$allSql, $totalAffected];
719+
};
720+
721+
[$allSql, $totalAffected] = $this->db->transaction($callback);
722+
723+
return $this->testMode ? implode('; ', $allSql) : $totalAffected;
724+
}
725+
726+
/**
727+
* Construit les bindings pour une requête bulk update
728+
*/
729+
protected function buildBulkUpdateBindings(array $chunk, string $column, array $updateColumns): array
730+
{
731+
$bindings = [];
732+
733+
foreach ($updateColumns as $updateColumn) {
734+
foreach ($chunk as $row) {
735+
$value = $row[$updateColumn];
736+
if (!($value instanceof Expression) && $value !== null) {
737+
$bindings[] = $value;
738+
}
739+
$bindings[] = $row[$column];
740+
}
741+
}
742+
743+
$ids = array_column($chunk, $column);
744+
$bindings = array_merge($bindings, $ids);
745+
746+
return $bindings;
747+
}
748+
648749
/**
649750
* Exécute une requête de remplacement.
650751
*

src/Builder/Compilers/QueryCompiler.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,52 @@ public function compileUpdate(BaseBuilder $builder): string
149149
return $this->compileUpdateStandard($builder);
150150
}
151151

152+
/**
153+
* Compile une requête de mise à jour en masse
154+
*
155+
* @param array $chunk Données du lot
156+
* @param string $column Colonne d'identification
157+
* @param array $updateColumns Colonnes à mettre à jour
158+
*/
159+
public function compileBulkUpdate(BaseBuilder $builder, array $chunk, string $column, array $updateColumns): string
160+
{
161+
$table = $this->db->escapeIdentifiers($builder->getTable());
162+
$columnEscaped = $this->db->escapeIdentifiers($column);
163+
164+
// Construction du CASE WHEN pour chaque colonne à mettre à jour
165+
$updateParts = [];
166+
foreach ($updateColumns as $updateColumn) {
167+
$caseStatement = $this->buildCaseStatement($chunk, $updateColumn, $column);
168+
$updateParts[] = $this->db->escapeIdentifiers($updateColumn) . ' = ' . $caseStatement;
169+
}
170+
171+
// Construction de la clause WHERE IN
172+
$ids = array_column($chunk, $column);
173+
$placeholders = implode(', ', array_fill(0, count($ids), '?'));
174+
175+
return "UPDATE {$table} SET " . implode(', ', $updateParts) . " WHERE {$columnEscaped} IN ({$placeholders})";
176+
}
177+
178+
/**
179+
* Construit une clause CASE WHEN pour une colonne spécifique
180+
*
181+
* @param array $chunk Données du lot
182+
* @param string $updateColumn Colonne à mettre à jour
183+
* @param string $column Colonne d'identification
184+
*/
185+
protected function buildCaseStatement(array $chunk, string $updateColumn, string $column): string
186+
{
187+
$cases = [];
188+
$column = $this->db->escapeIdentifiers($column);
189+
190+
foreach ($chunk as $row) {
191+
$value = $this->wrapValue($row[$updateColumn]);
192+
$cases[] = "WHEN {$column} = ? THEN {$value}";
193+
}
194+
195+
return "CASE " . implode(' ', $cases) . " END";
196+
}
197+
152198
/**
153199
* Compilation standard sans jointure
154200
*/

0 commit comments

Comments
 (0)