Banyak operasi database tidak berdiri sendiri: memindahkan saldo melibatkan dua UPDATE, membuat pesanan butuh insert ke beberapa tabel sekaligus. Jika di tengah proses terjadi kegagalan — koneksi putus, saldo tidak cukup — data akan tertinggal setengah jalan dan menjadi tidak konsisten. Transaction adalah mekanisme database untuk membungkus rangkaian query menjadi satu kesatuan: seluruhnya tersimpan permanen, atau seluruhnya dibatalkan seolah tidak pernah dijalankan. Pada chapter ini kita akan mempraktikkan beginTransaction(), commit(), dan rollBack() pada SQLite in-memory, lalu melihat pola penanganan error yang benar di sekelilingnya.
Konsep Dasar Transaksi #
Transaksi menjamin sifat atomicity: rangkaian query di dalamnya dianggap satu operasi tunggal yang tidak bisa terpecah. Database menjalankan query di dalam transaksi pada area kerja semu — perubahan baru benar-benar disimpan ketika transaksi di-commit, dan seluruh jejaknya dihapus ketika di-rollback.
Tiga method PDO mengendalikan siklus hidup transaksi: beginTransaction() membuka transaksi, commit() menyimpan semua perubahan sejak transaksi dibuka, dan rollBack() membatalkannya. Method inTransaction() melaporkan status transaksi aktif pada koneksi.
commit() Menyimpan Perubahan Permanen #
Mari kita mulai dari alur paling sederhana: buka transaksi, jalankan beberapa query, lalu simpan:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, nama TEXT, total INTEGER)");
$pdo->beginTransaction();
$pdo->exec("INSERT INTO pesanan (nama, total) VALUES ('PSN-001', 150000)");
$pdo->exec("INSERT INTO pesanan (nama, total) VALUES ('PSN-002', 275000)");
$pdo->commit();
echo "pesanan tersimpan: " . $pdo->query("SELECT COUNT(*) FROM pesanan")->fetchColumn() . "\n";
echo "inTransaction: " . var_export($pdo->inTransaction(), true) . "\n";Output yang Dihasilkan:
pesanan tersimpan: 2
inTransaction: falseDua INSERT di atas berada di antara beginTransaction() dan commit(). Baris pertama mengonfirmasi bahwa kedua pesanan benar-benar tersimpan, sedangkan baris kedua menunjukkan bahwa commit() sekaligus menutup transaksi — inTransaction() kembali bernilai false setelahnya. Koneksi yang sama dapat dipakai membuka transaksi baru kapan saja.
rollBack() Membatalkan Seluruh Perubahan #
Kebalikan dari commit(), rollBack() menghapus semua perubahan sejak transaksi dibuka. Perhatikan percobaan berikut:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, nama TEXT, total INTEGER)");
$pdo->beginTransaction();
$pdo->exec("INSERT INTO pesanan (nama, total) VALUES ('PSN-001', 150000)");
echo "di dalam transaksi: " . $pdo->query("SELECT COUNT(*) FROM pesanan")->fetchColumn() . "\n";
$pdo->rollBack();
echo "setelah rollBack: " . $pdo->query("SELECT COUNT(*) FROM pesanan")->fetchColumn() . "\n";Output yang Dihasilkan:
di dalam transaksi: 1
setelah rollBack: 0Di dalam transaksi, query SELECT pada koneksi yang sama melihat data semu tersebut — jumlah baris terbaca 1. Setelah rollBack(), baris itu lenyap sepenuhnya dan tabel kembali kosong. Tidak ada kondisi tengah: transaksi yang tidak diakhiri commit() akan dibatalkan begitu koneksi ditutup.
Studi Kasus: Transfer Antar Rekening #
Contoh nyata kekuatan transaksi adalah pemindahan saldo. Prosesnya membutuhkan dua UPDATE: mengurangi saldo pengirim dan menambah saldo penerima. Jika pengirim ternyata tidak punya cukup saldo, keduanya harus batal — kalau tidak, uang hilang tanpa jejak:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE rekening (nomor TEXT PRIMARY KEY, saldo INTEGER)");
$pdo->exec("INSERT INTO rekening VALUES ('BUDI', 100000), ('SITI', 50000)");
function transfer(PDO $pdo, string $dari, string $ke, int $jumlah): void
{
$pdo->beginTransaction();
try {
$kurangi = $pdo->prepare("UPDATE rekening SET saldo = saldo - :jumlah WHERE nomor = :nomor");
$kurangi->execute(['jumlah' => $jumlah, 'nomor' => $dari]);
$cek = $pdo->prepare("SELECT saldo FROM rekening WHERE nomor = ?");
$cek->execute([$dari]);
if ((int) $cek->fetchColumn() < 0) {
throw new RuntimeException("saldo $dari tidak cukup");
}
$tambah = $pdo->prepare("UPDATE rekening SET saldo = saldo + :jumlah WHERE nomor = :nomor");
$tambah->execute(['jumlah' => $jumlah, 'nomor' => $ke]);
$pdo->commit();
echo "transfer Rp$jumlah dari $dari ke $ke berhasil\n";
} catch (RuntimeException $e) {
$pdo->rollBack();
echo "transaksi dibatalkan: {$e->getMessage()}\n";
}
}
transfer($pdo, "BUDI", "SITI", 150000);
transfer($pdo, "BUDI", "SITI", 30000);
foreach ($pdo->query("SELECT nomor, saldo FROM rekening ORDER BY nomor", PDO::FETCH_ASSOC) as $row) {
echo "{$row['nomor']}: Rp{$row['saldo']}\n";
}Output yang Dihasilkan:
transaksi dibatalkan: saldo BUDI tidak cukup
transfer Rp30000 dari BUDI ke SITI berhasil
BUDI: Rp70000
SITI: Rp80000Mari kita bedah alurnya langkah demi langkah:
- Panggilan pertama meminta transfer Rp150.000 padahal saldo BUDI hanya Rp100.000. Setelah
UPDATEpertama, saldo BUDI sempat menjadi-50000di dalam transaksi. - Pemeriksaan saldo menemukan nilai negatif dan melempar
RuntimeException; blokcatchmemanggilrollBack()sehingga saldo BUDI dikembalikan ke100000— pengurangan yang sudah dijalankan ikut terhapus. - Panggilan kedua sebesar Rp30.000 lolos pemeriksaan, sehingga
commit()menyimpan keduaUPDATEsecara permanen. - Saldo akhir membuktikan hanya transfer kedua yang berdampak.
Tanpa transaksi, skenario panggilan pertama akan menyimpan saldo -50000 — uang berkurang tanpa penerima. Dengan transaksi, kedua UPDATE terikat satu sama lain: keduanya berhasil, atau keduanya tidak pernah terjadi.
Pola try-catch dengan rollBack #
Pada contoh transfer di atas, kegagalan bisnis (saldo kurang) memicu rollback secara eksplisit. Pola yang lebih tahan banting menangkap semua jenis kegagalan — termasuk PDOException dari query yang error — lalu membatalkan transaksi bila masih aktif:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE log (id INTEGER PRIMARY KEY, pesan TEXT)");
$pdo->beginTransaction();
try {
$pdo->exec("INSERT INTO log (pesan) VALUES ('mulai proses')");
throw new RuntimeException("koneksi ke payment gateway terputus");
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo "dibatalkan: {$e->getMessage()}\n";
}
echo "baris log tersimpan: " . $pdo->query("SELECT COUNT(*) FROM log")->fetchColumn() . "\n";Output yang Dihasilkan:
dibatalkan: koneksi ke payment gateway terputus
baris log tersimpan: 0Tiga keputusan pada pola ini layak dicermati. Pertama, blok catch menangkap Throwable — bukan hanya PDOException — supaya exception apa pun dari logika bisnis ikut memicu pembatalan. Kedua, pemeriksaan inTransaction() menjaga rollBack() tidak dipanggil pada keadaan yang sudah tidak valid (misalnya transaksi sudah otomatis dibatalkan oleh driver). Ketiga, urutannya selalu: batalkan dulu, baru laporkan atau lempar ulang exception-nya.
Transaksi dan DDL #
SQLite termasuk database yang memperlakukan DDL (CREATE TABLE, DROP TABLE) sebagai bagian transaksi, sehingga pembuatan tabel pun bisa di-rollback:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE kategori (id INTEGER PRIMARY KEY, nama TEXT)");
$pdo->beginTransaction();
$pdo->exec("INSERT INTO kategori (nama) VALUES ('utama')");
$pdo->exec("CREATE TABLE sementara (id INTEGER PRIMARY KEY)");
$pdo->rollBack();
echo "baris kategori: " . $pdo->query("SELECT COUNT(*) FROM kategori")->fetchColumn() . "\n";
try {
$pdo->query("SELECT * FROM sementara");
echo "tabel sementara masih ada\n";
} catch (PDOException $e) {
echo "tabel sementara ikut ter-rollback\n";
}Output yang Dihasilkan:
baris kategori: 0
tabel sementara ikut ter-rollbackRingkasan #
beginTransaction() membuka area kerja semu, commit() mengukuhkan seluruh perubahan, dan rollBack() menghapusnya bersih — dengan inTransaction() sebagai pemeriksa status. Studi kasus transfer menunjukkan atomicity menyelamatkan konsistensi data, pola catch (Throwable) menjadikan pembatalan otomatis, dan catatan DDL mengingatkan perbedaan antar database. Ada kalanya kita perlu membatalkan hanya sebagian transaksi atau merangkai transaksi bersarang — teknik savepoint dan helper transaksi akan kita bahas pada chapter Transaction Management
.