Home / Artikel / Web Development
Web Development

Database Transaction di PHP: Cara Mencegah Pesanan Tersimpan Setengah Jalan

Satu proses checkout bisa menyentuh beberapa tabel sekaligus. Dengan database transaction, aplikasi dapat memastikan semua perubahan berhasil bersama-sama atau dibatalkan ketika salah satu langkah gagal.

Database Transaction di PHP: Cara Mencegah Pesanan Tersimpan Setengah Jalan

Bayangkan pengguna membayar sebuah pesanan. Aplikasi kemudian perlu membuat data order, mengurangi stok, mencatat pembayaran, dan menyimpan alamat pengiriman. Jika proses berhenti setelah stok berkurang tetapi sebelum data pembayaran tersimpan, sistem memiliki masalah yang sulit dijelaskan: barang berkurang, tetapi pesanan terlihat belum dibayar.

Kasus seperti ini bukan hanya terjadi karena server mati. Error pada kode, koneksi database yang terputus, validasi yang terlambat, atau request yang habis waktu juga bisa membuat proses multi-langkah berhenti di tengah jalan. Di sinilah database transaction membantu.

Apa itu database transaction?

Database transaction adalah cara menjalankan beberapa operasi database sebagai satu kesatuan. Hasil akhirnya hanya memiliki dua kemungkinan: seluruh operasi berhasil, atau perubahan yang sudah dilakukan dibatalkan.

Dalam praktiknya, transaction biasanya memiliki tiga tahap:

  1. Begin: memberi tahu database bahwa serangkaian operasi baru dimulai.
  2. Commit: menyimpan seluruh perubahan jika semua langkah berhasil.
  3. Rollback: membatalkan perubahan jika terjadi kesalahan.

Analogi sederhananya adalah memindahkan barang dari gudang A ke gudang B. Tidak cukup hanya mengurangi jumlah barang di gudang A. Barang juga harus ditambahkan ke gudang B. Jika penambahan gagal, pengurangan dari gudang A harus dibatalkan agar jumlah total tetap benar.

Mengapa proses checkout perlu transaction?

Proses bisnis yang menyentuh lebih dari satu tabel biasanya layak dipertimbangkan untuk menggunakan transaction. Contohnya:

  • Membuat pesanan dan detail item pesanan.
  • Mengurangi stok sekaligus mencatat riwayat inventaris.
  • Mencatat pembayaran dan mengubah status pesanan.
  • Membuat akun pengguna beserta profil dan pengaturan awalnya.
  • Memindahkan saldo dari satu akun ke akun lain.

Tanpa transaction, setiap query dapat tersimpan sendiri-sendiri. Jika query ketiga gagal, query pertama dan kedua belum tentu ikut dibatalkan. Aplikasi kemudian perlu melakukan perbaikan manual atau membuat proses kompensasi yang lebih rumit.

Contoh transaction dengan PDO

PDO, atau PHP Data Objects, menyediakan method bawaan untuk mengelola transaction. Contoh berikut menunjukkan alur sederhana saat membuat pesanan.

<?php

$pdo->beginTransaction();

try {
    $stmt = $pdo->prepare(
        "INSERT INTO orders (user_id, total, status)
         VALUES (:user_id, :total, 'pending')"
    );
    $stmt->execute([
        ':user_id' => $userId,
        ':total' => $total
    ]);

    $orderId = $pdo->lastInsertId();

    $stmt = $pdo->prepare(
        "INSERT INTO order_items (order_id, product_id, quantity, price)
         VALUES (:order_id, :product_id, :quantity, :price)"
    );
    $stmt->execute([
        ':order_id' => $orderId,
        ':product_id' => $productId,
        ':quantity' => $quantity,
        ':price' => $price
    ]);

    $stmt = $pdo->prepare(
        "UPDATE products
         SET stock = stock - :quantity
         WHERE id = :product_id AND stock >= :quantity"
    );
    $stmt->execute([
        ':quantity' => $quantity,
        ':product_id' => $productId
    ]);

    if ($stmt->rowCount() !== 1) {
        throw new RuntimeException('Stok tidak mencukupi');
    }

    $pdo->commit();
} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }

    throw $e;
}

Pada contoh tersebut, data order, detail order, dan pengurangan stok baru dianggap final setelah commit() berhasil. Jika stok tidak cukup atau query lain menghasilkan error, rollBack() mengembalikan database ke kondisi sebelum transaction dimulai.

Transaction bukan pengganti validasi

Transaction menjaga konsistensi perubahan, tetapi bukan berarti semua masalah otomatis selesai. Validasi tetap perlu dilakukan sebelum dan selama proses.

Misalnya, aplikasi tetap harus memeriksa apakah pengguna memiliki izin, apakah produk masih aktif, apakah jumlah pembelian masuk akal, dan apakah harga yang digunakan berasal dari sumber yang dapat dipercaya. Transaction hanya memastikan perubahan database diperlakukan sebagai satu paket.

Perhatikan juga validasi stok pada query UPDATE. Kondisi stock >= :quantity lebih aman daripada membaca stok terlebih dahulu lalu melakukan pengurangan pada query terpisah tanpa perlindungan. Dengan begitu, database ikut memastikan stok tidak menjadi negatif.

Kesalahan yang sering terjadi

Memulai transaction terlalu terlambat

Jika aplikasi sudah melakukan beberapa perubahan sebelum beginTransaction(), perubahan tersebut tidak ikut dibatalkan. Mulailah transaction sebelum operasi yang harus diperlakukan sebagai satu kesatuan.

Memanggil API eksternal di dalam transaction

Transaction database sebaiknya tidak dibiarkan terbuka saat aplikasi menunggu payment gateway, layanan pengiriman, atau API lain. Durasi yang terlalu panjang dapat menahan lock dan meningkatkan risiko deadlock.

Pola yang lebih sehat adalah menyimpan pesanan dengan status awal, melakukan proses eksternal di luar transaction, lalu memperbarui status berdasarkan hasilnya. Untuk proses yang lebih kompleks, gunakan queue atau mekanisme retry yang memiliki batas jelas.

Lupa menangani exception

Jangan hanya memanggil commit() setelah query. Bungkus proses dengan try-catch dan pastikan rollback dilakukan ketika terjadi exception. Setelah rollback, error tetap perlu dicatat agar bisa diperiksa oleh developer.

Menggunakan storage engine yang tidak mendukung transaction

Transaction membutuhkan dukungan dari storage engine database. Pada MySQL, gunakan engine yang mendukung transaction, seperti InnoDB. Jika tabel menggunakan engine yang tidak mendukungnya, pemanggilan rollback tidak akan memberikan perlindungan seperti yang diharapkan.

Bagaimana dengan Laravel?

Framework seperti Laravel menyediakan pembungkus transaction agar kode lebih ringkas. Contoh sederhananya:

DB::transaction(function () use ($userId, $productId, $quantity) {
    $order = Order::create([
        'user_id' => $userId,
        'status' => 'pending'
    ]);

    $order->items()->create([
        'product_id' => $productId,
        'quantity' => $quantity
    ]);

    Product::where('id', $productId)
        ->where('stock', '>=', $quantity)
        ->decrement('stock', $quantity);
});

Framework membantu mengelola commit dan rollback, tetapi logika bisnisnya tetap harus dirancang dengan benar. Jika pengurangan stok gagal secara diam-diam atau tidak ada pengecekan hasil query, transaction saja belum cukup.

Apa artinya bagi kita?

Transaction adalah alat untuk menjaga database tetap masuk akal ketika satu aksi pengguna menghasilkan banyak perubahan. Ia sangat berguna untuk proses yang tidak boleh meninggalkan keadaan setengah jadi.

Yang bisa dilakukan sekarang:

  • Petakan fitur yang mengubah lebih dari satu tabel.
  • Tentukan perubahan mana yang harus berhasil bersama-sama.
  • Gunakan beginTransaction(), commit(), dan rollBack() secara konsisten.
  • Pastikan tabel MySQL menggunakan engine yang mendukung transaction.
  • Uji skenario gagal: stok habis, koneksi terputus, query error, dan request diulang.
  • Hindari menunggu API eksternal selama transaction masih terbuka.

Aplikasi yang baik bukan hanya mampu menyelesaikan skenario normal. Ia juga harus tetap menjaga data ketika sesuatu gagal. Database transaction memberi fondasi penting untuk membangun proses web yang lebih dapat dipercaya.

– Rio Yotto @rioyotto