مقاله

Database Transaction در Laravel — DB::transaction و rollback امن

Database Transaction در Laravel — DB::transaction و rollback امن

تراکنش دیتابیس برای ثبت سفارش، انتقال موجودی و عملیات چندجدولی — با transaction، deadlock و تست.

سفارش ساخته شد اما موجودی کم نشد، یا آیتم ثبت شد و سبد پاک نشد. بدون تراکنش، چند نوشتن وابسته می‌توانند نیمه‌کاره بمانند. Transaction یعنی atomicity: یا همه تغییرات commit می‌شوند یا هیچ‌کدام. در Laravel معمولاً با DB::transaction() این کار را می‌کنید. مقایسه موتورها جای دیگری است — MySQL در مقابل MariaDB؛ این مقاله روی معنای تراکنش و اشتباهات رایج تمرکز دارد.

Atomicity یعنی چه؟

مجموعه‌ای از queryها یک واحد منطقی می‌سازند (مثلاً ایجاد سفارش + آیتم‌ها + کاهش stock). اگر وسط کار exception رخ دهد، دیتابیس به حالت قبل برمی‌گردد. تراکنش به‌تنهایی همه race conditionها را حل نمی‌کند؛ برای رقابت روی یک ردیف گاهی lock یا استراتژی موجودی جدا لازم است.

شکل توصیه‌شده: DB::transaction

use Illuminate\Support\Facades\DB;

DB::transaction(function () use ($items) {
    $order = Order::create([/* ... */]);

    foreach ($items as $item) {
        $order->items()->create($item);
        Product::whereKey($item['product_id'])
            ->decrement('stock', $item['qty']);
    }

    Cart::where('user_id', auth()->id())->delete();
});

در Laravel ۱۲، امضای متد به‌صورت transaction(Closure $callback, int $attempts = 1) است. اگر closure با exception خارج شود، rollback انجام می‌شود و exception دوباره پرتاب می‌شود.

مقدار برگشتی

$order = DB::transaction(fn () => app(PlaceOrderAction::class)->handle($dto));

خروجی closure همان خروجی DB::transaction است — مناسب Action/Service.

begin / commit / rollBack دستی

DB::beginTransaction();
try {
    // ...
    DB::commit();
} catch (Throwable $e) {
    DB::rollBack();
    throw $e;
}

وقتی کنترل جریان پیچیده‌تر از یک closure است ممکن است لازم شود؛ در بیشتر کدهای اپلیکیشن، فراموش کردن rollBack ریسک دارد — ترجیح با closure است.

خطاهای کسب‌وکار در مقابل exception غیرمنتظره

اگر اعتبارسنجی یا قانون دامنه شکست بخورد، بهتر است قبل از شروع تراکنش یا با پرتاب exception کنترل‌شده از commit ناقص جلوگیری کنید. «بلعیدن» exception داخل تراکنش بدون rollback آگاهانه، منبع باگ‌های ساکت است.

Deadlock و attempts

DB::transaction(function () {
    // بخش حساس به قفل ردیف
}, 5);

پارامتر دوم تعداد تلاش‌هاست. در صورت deadlock/serialization failure، Laravel تا سقف attempts دوباره تلاش می‌کند. این جادو نیست: تراکنش را کوتاه نگه دارید و ترتیب قفل‌ها را در کدهای هم‌زمان یکسان کنید.

تراکنش تو در تو و محدودیت‌ها

Laravel برای سطوح تو در تو از savepoint استفاده می‌کند. این به معنی «دو تراکنش کاملاً مستقل روی یک connection» نیست. به رفتار تو در تو تکیه کور نکنید؛ ترجیحاً یک مرز تراکنش روشن در بالاترین use case داشته باشید.

afterCommit برای اثر جانبی

DB::afterCommit(function () use ($order) {
    SendOrderConfirmation::dispatch($order);
});

اگر Job را قبل از commit dispatch کنید، worker ممکن است ردیفی ببیند که هنوز commit نشده یا بعداً rollback شده. ایمیل، HTTP بیرونی، آپلود نهایی فایل و صف معمولاً خارج از تراکنش یا با afterCommit می‌روند. تراکنش فایل‌سیستم یا درخواست HTTP را پوشش نمی‌دهد.

Eloquent events داخل تراکنش

رویدادهایی مثل created ممکن است قبل از commit شنوندگان را اجرا کنند. اگر Listener به داده commit‌شده یا سیستم بیرونی وابسته است، کار را به afterCommit موکول کنید — Event و Listener.

تست commit و rollback

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PlaceOrderTransactionTest extends TestCase
{
    use RefreshDatabase;

    public function test_rolls_back_when_closure_fails(): void
    {
        try {
            DB::transaction(function () {
                Order::factory()->create();
                throw new RuntimeException('fail');
            });
        } catch (RuntimeException) {
        }

        $this->assertSame(0, Order::count());
    }
}

چند connection

هر connection تراکنش خودش را دارد. commit اتمی خودکار بین دو دیتابیس جدا (distributed XA) را Laravel برای شما حل نمی‌کند.

ضدالگوها

  • تراکنش طولانی با کار CPU/HTTP داخل آن — قفل را نگه می‌دارید
  • فراموش تراکنش در انتقال موجودی/پول
  • اتکا به تراکنش به‌جای طراحی درست هم‌زمانی
  • قاتی کردن با چرخه soft delete یا بازیابی Job — آن‌ها مقالات جدا دارند: Soft Deletes، Failed jobs

جمع‌بندی

هر نوشتن چندجدولی که باید با هم بماند → یک مرز DB::transaction روشن، اثر جانبی بعد از commit، تراکنش کوتاه، و تست rollback. Query Builder و Eloquent هر دو داخل همان تراکنش کار می‌کنند؛ روابط polymorphic هم از همین قاعده پیروی می‌کنند.

پیاده‌سازی سیستم سفارش امن

اشتراک‌گذاری