š„ Bulk Update Multiple Records with Separate Data ā Laravel
As a rule of thumb, we should never run database queries inside a for-loop!
āDatabase transactionā is an expensive operation.
For example, letās say we designed an inventory software and itās being used in production for a year, and reached 1,000,000 transactions.
Suddenly, we learnt that we didnāt add the VAT to our transactions. For the transactions in the future, itās pretty easy to deal with, maybe with a mutator.
class Transaction extends Model {
public $vat = 0.20; public function setPriceAttribute($value) {
$this->attributes['price'] += $value * $this->vat;
}}
Future records are pretty easy to deal with. However how are we going to edit the 1 million records from the past.
For editing data from the past, I prefer to create a Seeder.
php artisan make:seeder AddVatToTransactions
How NOT to do it?
If we had small number of transactions, we could do:
class AddVatToTransactions extends Seeder { public function run() {
$vat = 0.20; $transactions = Transaction::get(); foreach ($transactions as $transaction) {
$transaction->price += $transaction->price * $vat
$transaction->save();
}
}
}
However, running it in a loop of 1 million and making a ādatabase transactionā in each iteration of the loop ā not a good idea! (Spoiler Alert: Itāll freeze your system š)
Then, how to do it?
Again, in ourAddVatToTransactions
Seeder:
The idea in mysql query is āCASE Statementsā
UPDATE db.transactions
SET PRICE = CASE
WHEN id = 3 THEN 500
WHEN id = 4 THEN 300
END
WHERE ID IN (3, 4)
Now, letās do it in 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);
}
This will make one database transaction to write it all your updates.
š£ I can hear some of you saying: āItās still FREEZINGā
So.. Optimizing it even further:
#1: āSelectā only the data you need from the database to consume less RAM.
In our example, we only use āidā and āpriceā columns. So letās only select them.
$transactions = Transaction::select('id', 'price')->get();
#2: āChunkā your collection to separate your transaction to multiple database transactions.
In Laravel, you can chunk collections like
Transaction::get()->chunk(5000);
Letās apply all in our example
Here, first we divide our $transactions
collection into 5000 chunks and we do a ādatabase transactionā per 5k records at once.
Hope you like this trick!
Please let me know what you think in the comments or simply add a clap if you like it! š
Happy coding!