Все мы знаем, что запускать в цикле запросы к базе данных – плохое решение.
Транзакция к базе данных очень дорогостоящая операция
Допустим, мы разработали систему учета финансов и пользовались ею в течении года. За это время накопилось более миллиона записей в таблице успешных сделок.
Нам поставили задачу, что необходимо для всех цен добавить НДС. А чтобы аналитика не поплыла, НДС надо добавить и для старых записей. Пример так себе, но все же.
Для новых сделок задача простая, можно воспользоваться мутатором:
class Transaction extends Model { public $vat = 0.20; public function setPriceAttribute($value) { $this->attributes['price'] += $value * $this->vat; } }
Но как поступить с историческими данными. Какая первая мысль поступить?
Создаем сидер
php artisan make:seeder AddVatToTransactions
Если у нас не много записей, то мы могли бы поступить следующим образом:
class AddVatToTransactions extends Seeder { public function run() { $vat = 0.20; $transactions = Transaction::get(); foreach ($transactions as $transaction) { $transaction->price += $transaction->price * $vat; $transaction->save(); } } }
Но если у нас будет миллион записей, то скорее всего наш сайт повиснет от огромного кол-ва запросов к бд.
Есть мысль заменить множество отдельных запросов на один, используя case:
UPDATE db.transactions
SET PRICE = CASE
WHEN id = 3 THEN 500
WHEN id = 4 THEN 300
END
WHERE ID IN (3, 4)
На laravel у нас получится что-то вроде такого:
$vat = 0.20; $transactions = Transaction::get(); $cases = []; $ids = []; $params = []; foreach ($transactions as $transaction) { $cases[] = "WHEN {$transaction->id} then ?"; $params[] = $transaction->profit * $vat; $ids[] = $transaction->id; } $ids = implode(',', $ids); $cases = implode(' ', $cases); if (!empty($ids)) { \DB::update("UPDATE transactions SET `price` = CASE `id` {$cases} END WHERE `id` in ({$ids})", $params); }
Но система явно так же будет не в восторге от миллиона case’ов. Давайте воспользуемся коллекциями Laravel и разобьем на чанки. Т.к. нам требуются только id и profit, то добавим выборку только этих данных:
$vat = 0.20; $transactions = Transaction::select('id', 'price')->get(); foreach ($transactions->chunk(5000) as $chunk) { $cases = []; $ids = []; $params = []; foreach ($chunk as $transaction) { $cases[] = "WHEN {$transaction->id} then ?"; $params[] = $transaction->profit * $vat; $ids[] = $transaction->id; } $ids = implode(',', $ids); $cases = implode(' ', $cases); if (!empty($ids)) { \DB::update("UPDATE transactions SET `price` = CASE `id` {$cases} END WHERE `id` in ({$ids})", $params); } }