Perintah tulis pada chapter sebelumnya menyimpan risiko yang tidak terlihat pada contoh satu-perintah: perubahan nyata hampir selalu berjumlah lebih dari satu. Memindahkan satu unit barang dari pesanan A ke pesanan B menuntut dua UPDATE — dan bila proses berhenti di antara keduanya, barang sudah diambil dari A tetapi belum sampai di B. Data menjadi tidak konsisten tanpa jejak galat. Transaksi database mencegah kejadian tersebut dengan prinsip atomicity: rangkaian perubahan diperlakukan sebagai satu kesatuan yang berjalan sepenuhnya atau tidak sama sekali. Chapter ini menguji prinsip itu secara nyata di SQLite lewat skenario transfer yang sengaja gagal di tengah jalan.
Konsep: BEGIN, COMMIT, dan ROLLBACK #
Transaksi dibuka dengan BEGIN dan ditutup dengan salah satu dari dua perintah: COMMIT menjadikan seluruh perubahan permanen, atau ROLLBACK membatalkan semuanya seolah transaksi tidak pernah berjalan.
| Perintah | Peran |
|---|---|
BEGIN | membuka transaksi |
COMMIT | mengunci seluruh perubahan secara permanen |
ROLLBACK | membatalkan seluruh perubahan sejak BEGIN |
SAVEPOINT nama | menandai titik pulih di dalam transaksi |
ROLLBACK TO nama | membatalkan sebagian perubahan hingga titik pulih |
Atomicity adalah huruf A pada akronim ACID — empat jaminan (Atomicity, Consistency, Isolation, Durability) yang menjadi standar keandalan database modern. Bagian ini berfokus pada atomicity karena transaksi inilah fondasi dari ketiga jaminan lainnya.
Uji Nyata: Transfer yang Gagal di Tengah Jalan #
Skenarionya: memindahkan satu unit Kemeja Batik (produk 4) dari pesanan 1 ke pesanan 9. Kedua baris detail_pesanan harus berubah bersama — jumlah pesanan 1 menjadi satu, jumlah pesanan 9 menjadi dua. Skrip berikut menjalankan pemindahan di dalam transaksi, lalu kegagalan terjadi setelah UPDATE pertama:
from setup_toko import buat_database, tampilkan
koneksi = buat_database()
koneksi.isolation_level = None # transaksi hanya terjadi bila kita menulis BEGIN
koneksi.execute("BEGIN")
try:
koneksi.execute("""
UPDATE detail_pesanan
SET jumlah = 1, subtotal = 250000
WHERE pesanan_id = 1 AND produk_id = 4
""")
stok_tersedia = False # simulasi: layanan stok melaporkan barang habis
if not stok_tersedia:
raise RuntimeError("stok produk tidak tersedia")
koneksi.execute("""
UPDATE detail_pesanan
SET jumlah = 2, subtotal = 500000
WHERE pesanan_id = 9 AND produk_id = 4
""")
except RuntimeError as galat:
koneksi.execute("ROLLBACK")
print("Transaksi dibatalkan:", galat)
else:
koneksi.execute("COMMIT")
print("Transaksi dikomit")
tampilkan(koneksi, """
SELECT pesanan_id, produk_id, jumlah, subtotal
FROM detail_pesanan
WHERE produk_id = 4
ORDER BY pesanan_id
""")Output yang Dihasilkan:
Transaksi dibatalkan: stok produk tidak tersedia
pesanan_id | produk_id | jumlah | subtotal
1 | 4 | 2 | 500000
4 | 4 | 1 | 250000
9 | 4 | 1 | 250000Mari kita telaah alur skrip di atas:
koneksi.isolation_level = None: menonaktifkan transaksi implisit modulsqlite3. Tanpa pengaturan ini, Python membuka transaksi otomatis di belakang layar pada tiap perintah tulis — mode manual membuat batas transaksi terlihat jelas sesuai perintahBEGINyang kita tulis.koneksi.execute("BEGIN"): titik awal kesatuan perubahan; dari sini semua modifikasi berstatus sementara.UPDATEpertama (pesanan 1): berhasil dijalankan dan terlihat oleh koneksi ini — jumlah berubah menjadi 1.raise RuntimeError(...): kegagalan terjadi sebelumUPDATEkedua, meniru layanan eksternal yang mati di tengah proses.except ... ROLLBACK: seluruh perubahan sejakBEGINdibatalkan, termasukUPDATEpertama yang “sudah berhasil”.- Pemeriksaan akhir: semua baris produk 4 kembali ke nilai asli — pesanan 1 tetap
2 | 500000, pesanan 9 tetap1 | 250000— bukti atomicity bekerja.
Tanpa transaksi, UPDATE pertama akan langsung permanen dan tabel ditinggalkan dalam keadaan setengah dipindahkan. Dengan transaksi, kegagalan di titik mana pun selalu mengembalikan database ke kondisi konsisten.
SAVEPOINT: Rollback Sebagian #
Kadang kegagalan hanya menyerang sebagian pekerjaan: transfer utama berhasil, tetapi revisi tambahan yang menyusul ternyata keliru. SAVEPOINT menandai titik pulih di dalam transaksi sehingga ROLLBACK TO membatalkan hanya pekerjaan setelah titik tersebut:
from setup_toko import buat_database, tampilkan
koneksi = buat_database()
koneksi.isolation_level = None
koneksi.execute("BEGIN")
koneksi.execute("""
UPDATE detail_pesanan
SET jumlah = 1, subtotal = 250000
WHERE pesanan_id = 1 AND produk_id = 4
""")
koneksi.execute("""
UPDATE detail_pesanan
SET jumlah = 2, subtotal = 500000
WHERE pesanan_id = 9 AND produk_id = 4
""")
koneksi.execute("SAVEPOINT sebelum_revisi_harga")
koneksi.execute("""
UPDATE detail_pesanan
SET subtotal = 999999999
WHERE pesanan_id = 9 AND produk_id = 4
""") # revisi harga yang ternyata keliru
koneksi.execute("ROLLBACK TO sebelum_revisi_harga")
koneksi.execute("RELEASE sebelum_revisi_harga")
koneksi.execute("COMMIT")
tampilkan(koneksi, """
SELECT pesanan_id, produk_id, jumlah, subtotal
FROM detail_pesanan
WHERE produk_id = 4
ORDER BY pesanan_id
""")Output yang Dihasilkan:
pesanan_id | produk_id | jumlah | subtotal
1 | 4 | 1 | 250000
4 | 4 | 1 | 250000
9 | 4 | 2 | 500000Hasil akhirnya menunjukkan pembagian yang tepat: kedua UPDATE transfer tetap berlaku (1 | 250000 pada pesanan 1 dan 2 | 500000 pada pesanan 9), sedangkan revisi harga yang keliru (999999999) ikut terbatalkan. Urutan perintahnya membaca seperti daftar: SAVEPOINT menandai posisi aman, ROLLBACK TO memutar balik pekerjaan setelah posisi itu, dan RELEASE menghapus penanda sebelum COMMIT menutup transaksi. Dokumentasi lengkap perintah transaksi tersedia di dokumentasi SQLite
dan SQL Commands PostgreSQL
.
Transaksi menjawab pertanyaan “bagaimana data tetap utuh saat proses gagal”. Pertanyaan penutup section ini berbeda: “mengapa query yang benar bisa berjalan lambat”. Chapter berikutnya, Optimasi Query Dasar, membaca execution plan dengan EXPLAIN, membangun index pertama, dan menulis klausul WHERE yang tidak meniadakan index tersebut.