Transaksi mengelompokkan beberapa perubahan basis data menjadi satu kesatuan yang hanya berujung pada dua keadaan, yaitu diterapkan seluruhnya atau tidak diterapkan sama sekali. Topik ini membahas tiga metode transaksi, yaitu beginTransaction(), commit(), dan rollBack(), perilaku autocommit di sekeliling transaksi, serta pemulihan otomatis saat pengecualian tidak tertangkap. Contoh yang dipakai adalah pemindahan stok buku antar rak, karena perpindahan tersebut selalu terdiri atas dua perubahan yang harus sama-sama berhasil.
Satu kesatuan kerja yang tidak boleh terpecah#
Memindahkan tiga buku dari rak A-01 ke rak B-02 bukan satu pernyataan SQL, melainkan dua pernyataan UPDATE, yaitu mengurangi isi rak asal dan menambah isi rak tujuan. Apabila pernyataan pertama berhasil dan yang kedua gagal, stok tiga buku itu hilang dari dua rak sekaligus, dan tidak ada catatan yang menunjukkan ke mana perginya. Basis data tidak menolak keadaan semacam itu, karena masing-masing UPDATE adalah pernyataan yang sah bila dijalankan sendiri-sendiri.
Transaksi menyelesaikan masalah ini dengan menahan penerapan seluruh perubahan sampai keputusan akhir dinyatakan. Sifat berlaku seluruhnya atau batal seluruhnya ini disebut atomik, yaitu kesatuan kerja yang tidak dapat terpecah menjadi bagian yang dijalankan sebagian. Apabila semua pernyataan selesai tanpa kegagalan, transaksi diterapkan melalui commit() dan seluruh perubahan terlihat oleh koneksi lain sekaligus. Apabila ada satu kegagalan, rollback membatalkannya dan basis data kembali tepat ke keadaan sebelum transaksi dimulai, termasuk membatalkan pernyataan yang sudah terlanjur dijalankan di dalamnya.
Keputusan tentang batas kesatuan kerja berada di tangan kode aplikasi, bukan di tangan basis data. Basis data hanya menyediakan mekanismenya, sedangkan pemilihan pernyataan mana yang digabungkan menjadi tanggung jawab desain skripnya.
beginTransaction, commit, dan rollback#
Metode beginTransaction() menandai awal transaksi pada koneksi aktif, metode commit() menerapkan seluruh perubahannya, dan metode rollBack() membatalkannya sampai titik awal. Metode inTransaction() melaporkan apakah koneksi sedang berada di dalam transaksi, dan pemeriksaannya berguna sebelum memanggil commit() maupun rollBack(). Diagram berikut memperlihatkan alur lengkapnya, termasuk dua cabang akhirnya.
Skrip berikut mengeksekusi dua skenario pada tabel yang sama. Skenario pertama memindahkan tiga buku dan berakhir dengan commit(), sedangkan skenario kedua mencoba memindahkan 99 buku, jumlah yang melebihi stok rak asal, sehingga pengecualiannya sengaja dilempar dan ditangkap untuk memicu rollback.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:', null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->exec('CREATE TABLE rak (kode TEXT PRIMARY KEY, isi INTEGER NOT NULL)');
$pdo->exec("INSERT INTO rak (kode, isi) VALUES ('A-01', 10), ('B-02', 4)");
function pindahkan(PDO $pdo, string $asal, string $tujuan, int $jumlah): void
{
$stmt = $pdo->prepare('UPDATE rak SET isi = isi - :jumlah WHERE kode = :asal');
$stmt->execute([':jumlah' => $jumlah, ':asal' => $asal]);
$stmt = $pdo->prepare('UPDATE rak SET isi = isi + :jumlah WHERE kode = :tujuan');
$stmt->execute([':jumlah' => $jumlah, ':tujuan' => $tujuan]);
$stmt = $pdo->prepare('SELECT isi FROM rak WHERE kode = :asal');
$stmt->execute([':asal' => $asal]);
$stokAsal = (int) $stmt->fetchColumn();
if ($stokAsal < 0) {
throw new RuntimeException("Stok rak $asal menjadi $stokAsal, sehingga perubahan tidak sah");
}
}
$pdo->beginTransaction();
pindahkan($pdo, 'A-01', 'B-02', 3);
$pdo->commit();
echo 'Pemindahan 3 buku dari A-01 ke B-02: commit', "\n";
try {
$pdo->beginTransaction();
pindahkan($pdo, 'A-01', 'B-02', 99);
$pdo->commit();
} catch (RuntimeException $e) {
$pdo->rollBack();
echo 'Rollback: ', $e->getMessage(), "\n";
}
$stmt = $pdo->query('SELECT kode, isi FROM rak ORDER BY kode');
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $baris) {
echo $baris['kode'], ': ', $baris['isi'], ' buku', "\n";
}Pemindahan 3 buku dari A-01 ke B-02: commit
Rollback: Stok rak A-01 menjadi -92, sehingga perubahan tidak sah
A-01: 7 buku
B-02: 7 bukuPesan rollback menyebut stok rak A-01 menjadi -92, yang berarti pernyataan UPDATE pertama pada skenario kedua sudah terlanjur dijalankan sebelum pengecualian dilempar. Keadaan akhir pada dua baris terakhir menegaskan pembatalannya, karena isi rak A-01 kembali ke 7, yaitu keadaan tepat sebelum transaksi kedua dimulai, dan bukan -92. Fungsi pindahkan() sengaja tidak memanggil beginTransaction(), karena pembukaan transaksi adalah keputusan pemanggilnya, sedangkan fungsi itu hanya mengerjakan pernyataan dan pemeriksaan di dalam transaksi tersebut.
Autocommit dan pemulihan otomatis#
Di luar transaksi, PDO berjalan dalam mode autocommit, yaitu setiap pernyataan diterapkan segera setelah selesai. Pemanggilan beginTransaction() mematikan autocommit sampai commit() atau rollBack() dijalankan, dan setelahnya autocommit aktif kembali. Inilah sebabnya pernyataan tunggal yang ditulis tanpa transaksi tetap tersimpan, sedangkan pernyataan di dalam transaksi menunggu keputusan akhirnya.
Apabila pengecualian tidak tertangkap, skrip berhenti sebelum commit() sempat dijalankan. Pada keadaan itu, PDO membatalkan transaksi yang tertinggal secara otomatis ketika koneksinya ditutup pada akhir skrip, sehingga basis data tidak tertinggal dalam keadaan setengah selesai. Meskipun demikian, penangkapan eksplisit beserta rollBack() tetap lebih baik karena kode aplikasi dapat mencatat penyebabnya dan melanjutkan eksekusi dengan keadaan yang pasti. SQLite menandai pernyataan DDL (Data Definition Language) seperti CREATE TABLE sebagai bagian transaksi, sehingga tabel yang dibuat di dalam transaksi juga ikut dibatalkan oleh rollBack().
Kesalahan umum#
- Menumpuk transaksi tanpa menutup yang sebelumnya. Memanggil
beginTransaction()ketika transaksi masih terbuka melemparPDOException, dan keadaan ini biasanya muncul setelah pengecualian ditangkap tanparollBack(). PeriksainTransaction()atau pastikan setiaptrymembawarollBack()pada blok tangkapnya. - Menyisipkan pekerjaan lambat di dalam transaksi. Permintaan HTTP atau pemanggilan layanan lain yang berada di antara
beginTransaction()dancommit()membuat transaksi terbuka lama dan sumber daya basis data tertahan. Kumpulkan hanya pernyataan basis data di dalam transaksi. - Menganggap keberhasilan pernyataan pertama otomatis menyelamatkan pernyataan kedua. Tanpa transaksi, mode autocommit menyimpan setiap pernyataan begitu selesai, sehingga kegagalan di tengah menyisakan keadaan terpecah. Bungkus pasangan perubahan yang saling bergantung ke dalam satu transaksi.
- Melupakan
commit()pada jalur yang berhasil. Pernyataan di dalam transaksi tidak tersimpan sebelumcommit()dipanggil, termasuk ketika seluruh pemeriksaannya berhasil, sehingga perubahan yang dianggap selesai tetap tidak terlihat. Pastikan setiap jalur yang berakhir tanpa kegagalan memanggilcommit()sebelum transaksinya dipakai lagi.
Ringkasan#
beginTransaction()menandai awal kesatuan kerja,commit()menerapkan seluruh perubahannya, danrollBack()mengembalikan basis data ke titik awalnya.- Autocommit berlaku di luar transaksi, sehingga setiap pernyataan tunggal tersimpan segera, sedangkan pernyataan di dalam transaksi menunggu keputusan akhir.
- Pengecualian yang tidak tertangkap menyebabkan PDO membatalkan transaksi tersisa secara otomatis saat koneksi ditutup, tetapi penangkapan eksplisit memberi tempat untuk pencatatan dan pemulihan.
- Transaksi dijaga tetap pendek, dan
inTransaction()dipakai untuk memastikan keadaan koneksi sebelumcommit()maupunrollBack().
Referensi#
- Transactions and auto-commit — Manual PHP — Menjelaskan autocommit dan tiga metode transaksi pada PDO.
- PDO::beginTransaction — Manual PHP — Mendokumentasikan penandaan awal transaksi dan pengecualiannya.
- PDO::rollBack — Manual PHP — Menjelaskan pembatalan transaksi beserta syarat transaksi aktifnya.
- Transactions — SQLite
— Menetapkan perilaku
BEGIN,COMMIT, danROLLBACKpada SQLite.