Забезпечення узгодженості даних за допомогою транзакцій у Laravel

Перекладено ШІ
Оригінал: Laravel News
Оновлено: 16 серпня, 2025
У статті розглядаються можливості транзакцій у Laravel, які забезпечують збереження цілісності даних під час роботи з базою даних. Ви дізнаєтеся, як реалізувати атомарні операції та уникнути помилок, щоб ваші фінансові додатки працювали бездоганно. Чи готові ви дізнатися більше про потужні інструменти для роботи з транзакціями в Laravel

Транзакції в базі даних є важливим засобом для забезпечення цілісності даних у складних додатках. 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() прямо в класах моделей для більш семантичних операцій.