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

1
Перекладено ШІ
Оригінал: Laravel News
Оновлено: 08 січня, 2026
У статті розглядаються можливості транзакцій у 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() прямо в класах моделей для більш семантичних операцій.

Популярні

Logomark Logotype

Обробка геопросторових даних за допомогою Laravel Magellan

Ви готові відкрити нові горизонти у роботі з геопросторовими даними в Laravel? Дізнайтеся, як за допомогою PostGIS та пакету Laravel-Magellan можна легко зберігати, запитувати та маніпулювати інформацією про розташування, перетворюючи ваші проекти на вражаючі рішення у сфері картографії та геолокації!

Logomark Logotype

Журнал аудиту в Laravel

Хочете забезпечити повну прозорість у своїх Laravel-додатках? Пакет Laravel Audit Log допоможе вам детально відстежувати всі зміни моделей Eloquent та відповідати вимогам регуляторів. Читайте далі, щоб дізнатися, як цей потужний інструмент може підвищити надійність вашого проєкту

Logomark Logotype

Що нового в PHP 8.5

PHP 8.5 обіцяє безліч нових можливостей, таких як оператор Pipe, функції `array_first()` та `array_last()`, а також нове розширення URI. Чи готові ви дізнатися, як ці функції можуть спростити вашу розробку? Читайте далі, щоб дізнатися більше про ці захоплюючі нововведення