Database Transaction در Laravel — DB::transaction و rollback امن
سفارش ثبت شد اما آیتمهای سبد پاک نشد — یا برعکس. بدون transaction در محیط concurrent، داده ناسازگار میشود. Transaction یعنی «یا همه queryها موفق، یا هیچکدام». در Laravel با DB::transaction() ساده است؛ درک ACID برای debug production ضروری است.
سادهترین شکل
use Illuminate\Support\Facades\DB;
DB::transaction(function () {
$order = Order::create([...]);
foreach ($items as $item) {
$order->items()->create($item);
Product::where('id', $item['product_id'])->decrement('stock', $item['qty']);
}
Cart::where('user_id', auth()->id())->delete();
});
Exception = rollback خودکار.
دستی begin/commit/rollBack
DB::beginTransaction();
try {
// ...
DB::commit();
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
ترجیحاً closure transaction — کمتر فراموش rollBack.
return value
$order = DB::transaction(fn () => OrderService::place($dto));
تعداد retry — deadlock
DB::transaction(function () {
// ...
}, attempts: 5);
MySQL deadlock گاهی retry میخواهد — Laravel 8+ پارامتر attempts.
afterCommit — job بعد از commit
DB::afterCommit(function () use ($order) {
SendOrderConfirmation::dispatch($order);
});
queue قبل از commit = job روی داده commitنشده — خطرناک.
transaction و Eloquent events
created داخل transaction قبل از commit fire میشود — listener که به DB بیرون وابسته است از afterCommit استفاده کند.
تست transaction
it('rolls back on failure', function () {
try {
DB::transaction(function () {
Order::factory()->create();
throw new Exception('fail');
});
} catch (Exception) {}
expect(Order::count())->toBe(0);
});
با RefreshDatabase.
چند connection
transaction per connection — distributed transaction بین دو DB خودکار نیست.
anti-pattern
- transaction خیلی بلند (lock نگه داشتن)
- HTTP call خارجی داخل transaction
- فراموش transaction در transfer پول/موجودی
منطق در Service — نه Controller.
جمعبندی
هر عملیات multi-table که باید atomic باشد → transaction. Query Builder و Eloquent هر دو داخل transaction. Polymorphic هم همین rule.