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.

PerintahPeran
BEGINmembuka transaksi
COMMITmengunci seluruh perubahan secara permanen
ROLLBACKmembatalkan seluruh perubahan sejak BEGIN
SAVEPOINT namamenandai titik pulih di dalam transaksi
ROLLBACK TO namamembatalkan 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:

python
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:

text
Transaksi dibatalkan: stok produk tidak tersedia
pesanan_id | produk_id | jumlah | subtotal
1 | 4 | 2 | 500000
4 | 4 | 1 | 250000
9 | 4 | 1 | 250000

Mari kita telaah alur skrip di atas:

  1. koneksi.isolation_level = None: menonaktifkan transaksi implisit modul sqlite3. Tanpa pengaturan ini, Python membuka transaksi otomatis di belakang layar pada tiap perintah tulis — mode manual membuat batas transaksi terlihat jelas sesuai perintah BEGIN yang kita tulis.
  2. koneksi.execute("BEGIN"): titik awal kesatuan perubahan; dari sini semua modifikasi berstatus sementara.
  3. UPDATE pertama (pesanan 1): berhasil dijalankan dan terlihat oleh koneksi ini — jumlah berubah menjadi 1.
  4. raise RuntimeError(...): kegagalan terjadi sebelum UPDATE kedua, meniru layanan eksternal yang mati di tengah proses.
  5. except ... ROLLBACK: seluruh perubahan sejak BEGIN dibatalkan, termasuk UPDATE pertama yang “sudah berhasil”.
  6. Pemeriksaan akhir: semua baris produk 4 kembali ke nilai asli — pesanan 1 tetap 2 | 500000, pesanan 9 tetap 1 | 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:

python
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:

text
pesanan_id | produk_id | jumlah | subtotal
1 | 4 | 1 | 250000
4 | 4 | 1 | 250000
9 | 4 | 2 | 500000

Hasil 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.