Транзакції в базі даних є важливим засобом для забезпечення цілісності даних у складних додатках. Laravel пропонує потужні можливості для роботи з транзакціями, які гарантують, що пов’язані операції в базі даних виконуються атомарно — або всі зміни відбуваються, або жодна з них не застосовується.
Метод DB::transaction
в Laravel є найзручнішим способом для управління транзакціями:
use Illuminate\Support\Facades\DB;
DB::transaction(function () {
DB::table('accounts')->where('id', 1)->decrement('balance', 100);
DB::table('accounts')->where('id', 2)->increment('balance', 100);
});
Цей метод автоматично керує життєвим циклом транзакції, скасовуючи всі зміни, якщо в середині замикання виникне будь-яка помилка.
Розглянемо фінансовий застосунок, що обробляє операції з перерозподілу інвестиційних портфелів. Необхідно одночасно внести кілька змін в акаунти для підтримання точності фінансових записів:
<?php
namespace App\Services;
use App\Models\Account;
use App\Models\Transaction;
use App\Models\Portfolio;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class PortfolioRebalancingService
{
public function rebalancePortfolio(Portfolio $portfolio, array $adjustments): bool
{
return DB::transaction(function () use ($portfolio, $adjustments) {
$totalAdjustment = 0;
foreach ($adjustments as $adjustment) {
$account = Account::findOrFail($adjustment['account_id']);
if ($adjustment['amount'] < 0 && $account->balance < abs($adjustment['amount'])) {
throw new \Exception("Недостатньо коштів на рахунку {$account->id}");
}
$account->increment('balance', $adjustment['amount']);
$totalAdjustment += $adjustment['amount'];
Transaction::create([
'account_id' => $account->id,
'portfolio_id' => $portfolio->id,
'amount' => $adjustment['amount'],
'type' => $adjustment['amount'] > 0 ? 'credit' : 'debit',
'description' => 'Перерозподіл портфеля',
'processed_at' => Carbon::now(),
]);
}
if (abs($totalAdjustment) > 0.01) {
throw new \Exception('Суми транзакцій повинні зрівноважуватись');
}
$portfolio->update([
'last_rebalanced_at' => Carbon::now(),
'status' => 'balanced'
]);
return true;
}, 3);
}
public function transferFunds(Account $fromAccount, Account $toAccount, float $amount): void
{
DB::beginTransaction();
try {
if ($fromAccount->balance < $amount) {
throw new \Exception('Недостатньо коштів для переказу');
}
$fromAccount->decrement('balance', $amount);
$toAccount->increment('balance', $amount);
Transaction::create([
'account_id' => $fromAccount->id,
'amount' => -$amount,
'type' => 'transfer_out',
'reference_account_id' => $toAccount->id,
'processed_at' => Carbon::now(),
]);
Transaction::create([
'account_id' => $toAccount->id,
'amount' => $amount,
'type' => 'transfer_in',
'reference_account_id' => $fromAccount->id,
'processed_at' => Carbon::now(),
]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
public function batchUpdateHoldings(Portfolio $portfolio, array $holdings): void
{
DB::transaction(function () use ($portfolio, $holdings) {
$portfolio->holdings()->delete();
foreach ($holdings as $holding) {
$portfolio->holdings()->create([
'symbol' => $holding['symbol'],
'quantity' => $holding['quantity'],
'average_cost' => $holding['average_cost'],
'current_value' => $holding['current_value'],
]);
}
$totalValue = collect($holdings)->sum('current_value');
$portfolio->update(['total_value' => $totalValue]);
});
}
}
class SubscriptionManager
{
public function upgradePlan(User $user, Plan $newPlan): void
{
DB::transaction(function () use ($user, $newPlan) {
$currentSubscription = $user->subscription;
if ($currentSubscription) {
$remainingDays = $currentSubscription->ends_at->diffInDays(Carbon::now());
$creditAmount = ($currentSubscription->plan->price / 30) * $remainingDays;
$user->account->increment('credit_balance', $creditAmount);
$currentSubscription->update(['status' => 'cancelled']);
}
$user->subscriptions()->create([
'plan_id' => $newPlan->id,
'starts_at' => Carbon::now(),
'ends_at' => Carbon::now()->addMonth(),
'status' => 'active'
]);
$user->account->decrement('credit_balance', $newPlan->price);
if ($user->account->credit_balance < 0) {
throw new \Exception('Недостатньо коштів на рахунку');
}
});
}
}
Laravel підтримує механізми повторних спроб транзакцій для обробки блокувань та надає контроль рівня ізоляції для складніших сценаріїв. Фреймворк також інтегрує обробку транзакцій з моделями Eloquent, що дозволяє викликати User::transaction()
прямо в класах моделей для більш семантичних операцій.