← Back to posts

DB Transaction di Laravel

Tech2 min read

Waktu pertama kali bikin fitur yang melibatkan banyak operasi database sekaligus, saya sempat khawatir: “Kalau satu langkah gagal, data bakal berantakan nggak ya?” Setelah pakai Database Transaction, kekhawatiran itu hilang karena semua proses jadi berjalan lebih aman dan konsisten.

Kenapa Perlu Transaction?

Sebagai contoh. misalkan pada fitur top-up atau transfer saldo

// Tanpa Transaction - BERBAHAYA!
$sender = User::find(1);
$sender->balance -= 100000;
$sender->save();

// Tiba-tiba error di sini! Server crash atau apa...
// Uang udah kepotong tapi belum masuk ke penerima

$receiver = User::find(2);
$receiver->balance += 100000;
$receiver->save();

Dengan transaction, kalau ada error di mana aja, semuanya bakal di-rollback

Penggunaan Transaction di Laravel

Laravel punya beberapa metode penggunaan transaction, disini saya contohkan yang sering saya temui

1. Cara Otomatis (Sederhana)

Ini penulisan sederhana. Laravel akan otomatis rollback kalau ada exception.

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    $sender = User::find(1);
    $sender->balance -= 100000;
    $sender->save();
    
    $receiver = User::find(2);
    $receiver->balance += 100000;
    $receiver->save();
    
    // Kalau ada error di mana aja, otomatis rollback!
});

Cukup wrap operasi database dalam closure DB::transaction().

2. Cara Manual (Lebih Fleksibel)

Kalau butuh kontrol lebih, bisa pakai cara manual dengan beginTransaction, commit, dan rollback. Ini metode penulisan yang sering saya gunakan karena menurut saya ini yang lebih mudah dipahami

use Illuminate\Support\Facades\DB;

DB::beginTransaction();

try {
    $sender = User::find(1);
    $sender->balance -= 100000;
    $sender->save();
    
    $receiver = User::find(2);
    $receiver->balance += 100000;
    $receiver->save();
    
    DB::commit(); // Kalau semua oke, commit
    
} catch (\Exception $e) {
    DB::rollBack(); // Kalau ada error, rollback
    
    // Handle error
    return response()->json(['error' => $e->getMessage()], 500);
}

3. Return Value dari Transaction

Kamu juga bisa return value dari transaction closure:

$result = DB::transaction(function () {
    $order = Order::create([
        'user_id' => 1,
        'total' => 150000
    ]);
    
    $product = Product::find(10);
    $product->stock -= 1;
    $product->save();
    
    return $order; // Return order yang baru dibuat
});

return response()->json($result);

Contoh real-case sederhana

Transfer Saldo

public function transfer(Request $request)
{
    $validated = $request->validate([
        'receiver_id' => 'required|exists:users,id',
        'amount' => 'required|numeric|min:1000'
    ]);
    
    DB::transaction(function () use ($validated) {
        $sender = auth()->user();
        $receiver = User::findOrFail($validated['receiver_id']);
        $amount = $validated['amount'];
        
        // Validasi saldo cukup
        if ($sender->balance < $amount) {
            throw new \Exception('Saldo tidak cukup');
        }
        
        // Kurangi saldo pengirim
        $sender->balance -= $amount;
        $sender->save();
        
        // Tambah saldo penerima
        $receiver->balance += $amount;
        $receiver->save();
        
        // Catat history
        Transaction::create([
            'sender_id' => $sender->id,
            'receiver_id' => $receiver->id,
            'amount' => $amount,
            'type' => 'transfer'
        ]);
    });
    
    return response()->json(['message' => 'Transfer berhasil']);
}

Checkout Order

public function checkout(Request $request)
{
    $order = DB::transaction(function () use ($request) {
        // Buat order
        $order = Order::create([
            'user_id' => auth()->id(),
            'total' => $request->total,
            'status' => 'pending'
        ]);
        
        // Loop items
        foreach ($request->items as $item) {
            $product = Product::findOrFail($item['product_id']);
            
            // Cek stock
            if ($product->stock < $item['quantity']) {
                throw new \Exception("Stock {$product->name} tidak cukup");
            }
            
            // Kurangi stock
            $product->stock -= $item['quantity'];
            $product->save();
            
            // Buat order item
            OrderItem::create([
                'order_id' => $order->id,
                'product_id' => $product->id,
                'quantity' => $item['quantity'],
                'price' => $product->price
            ]);
        }
        
        return $order;
    });
    
    return response()->json($order);
}

Delete User dengan Relasi

public function deleteUser($userId)
{
    DB::transaction(function () use ($userId) {
        $user = User::findOrFail($userId);
        
        // Hapus semua posts user
        $user->posts()->delete();
        
        // Hapus semua comments user
        $user->comments()->delete();
        
        // Hapus user
        $user->delete();
    });
    
    return response()->json(['message' => 'User berhasil dihapus']);
}

Retry Transaction

Laravel juga punya fitur retry. Kalau transaction gagal karena deadlock, bisa otomatis retry.

DB::transaction(function () {
    // operasi database
}, 5); // Retry 5x kalau gagal

Nested Transaction

Laravel support nested transaction dengan savepoint. Tapi honestly, sebisa mungkin hindari nested transaction karena bikin kompleks dan sulit untuk dipahami kodenya.

DB::transaction(function () {
    User::create([...]);
    
    DB::transaction(function () {
        Post::create([...]);
    });
});

Kapan Pakai Transaction?

✅ Wajib Pakai:

  • Transfer uang/saldo
  • Checkout/order dengan multiple items
  • Update data yang saling bergantung
  • Delete dengan cascade (hapus user + posts + comments)
  • Create dengan relasi (create order + order items)

❌ Nggak Perlu:

  • Single insert/update yang independent
  • Read-only operations
  • Operasi yang nggak related

Kesalahan yang kadang terjadi

1. Lupa Rollback

// ❌ Bad
DB::beginTransaction();
try {
    // operasi
    DB::commit();
} catch (\Exception $e) {
    // Lupa rollback!
    return response()->json(['error' => $e->getMessage()]);
}

2. Transaction Terlalu Besar

// ❌ Bad
DB::transaction(function () {
    // 100 operasi database
    // Loop ratusan data
    // Proses berat
});

3. Catch Exception Tanpa Re-throw

// ❌ Bad
DB::transaction(function () {
    try {
        // operasi yang bisa error
    } catch (\Exception $e) {
        // Catch tapi nggak di-throw lagi
        // Transaction tetap commit!
    }
});

Database transaction di Laravel itu sederhana, namun ada beberapa hal yang perlu diingat:

  • Pakai DB::transaction() untuk auto-handling
  • Pakai beginTransaction(), commit(), rollback() kalau butuh kontrol lebih
  • Validasi dulu sebelum masuk transaction
  • Jangan lupa error handling

Dengan penggunaan transaction ini, operasi data yang dijalan akan lebih konsisten dan aman jika ada proses yang gagal.