Konsep transaction — kumpulan perubahan yang berlaku semua-atau-tidak-sama-sekali — sudah kita kuasai lewat BEGIN, COMMIT, dan ROLLBACK pada Chapter Transaksi Dasar: BEGIN, COMMIT, ROLLBACK. Di Python, modul sqlite3 mengambil alih sebagian pekerjaan tersebut: statement BEGIN tidak perlu kita tulis, karena modul ini membuka transaction secara otomatis di belakang layar. Bab ini membedah kapan transaction terbuka, bagaimana commit() dan rollback() bekerja, dan bagaimana mode autocommit mengembalikan kendali penuh ke tangan kita.
Transaction Implisit #
Secara bawaan (mode legacy), modul sqlite3 membuka transaction otomatis tepat sebelum statement DML pertama — INSERT, UPDATE, DELETE, atau REPLACE — dan transaction itu tetap terbuka sampai kita memanggil commit() atau rollback(). Statement DDL seperti CREATE TABLE tidak memicunya. Atribut Connection.in_transaction melaporkan statusnya secara langsung:
import os
import sqlite3
if os.path.exists("toko.db"):
os.remove("toko.db")
con = sqlite3.connect("toko.db")
cur = con.cursor()
cur.execute("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, isi TEXT)")
print("setelah CREATE TABLE:", con.in_transaction)
cur.execute("INSERT INTO pesanan (isi) VALUES (?)", ("kopi gayo",))
print("setelah INSERT:", con.in_transaction)
con.rollback()
print("setelah rollback:", con.in_transaction)
print("jumlah baris:", cur.execute("SELECT COUNT(*) FROM pesanan").fetchone()[0])
cur.execute("INSERT INTO pesanan (isi) VALUES (?)", ("gula aren",))
con.commit()
print("setelah commit:", con.in_transaction)
print("jumlah baris:", cur.execute("SELECT COUNT(*) FROM pesanan").fetchone()[0])
con.close()Output yang Dihasilkan:
setelah CREATE TABLE: False
setelah INSERT: True
setelah rollback: False
jumlah baris: 0
setelah commit: False
jumlah baris: 1Mari kita ikuti alurnya:
- Baris 11 (
print("setelah CREATE TABLE: ...")):CREATE TABLEberjalan dan langsung selesai —in_transactionbernilaiFalsekarena DDL tidak membuka transaction implisit. - Baris 12 (
cur.execute("INSERT ...")): modulsqlite3diam-diam menjalankanBEGINlebih dulu, baru statement INSERT —in_transactionkiniTrue. - Baris 15 (
con.rollback()): seluruh perubahan sejak BEGIN dibatalkan, termasuk INSERT kopi gayo; jumlah baris kembali0. - Baris 19-20 (
INSERTlalucon.commit()): INSERT gula aren masuk transaction baru, lalucommit()membuatnya permanen — jumlah baris1danin_transactionkembaliFalse.
commit, rollback, dan Blok with #
Dua metode Connection mengendalikan nasib transaction: commit() menyimpan semua perubahan, rollback() membuangnya. Blok with con: membungkus pasangan itu — commit saat blok sukses, rollback saat exception lewat — seperti yang sudah kita lihat pada Chapter Koneksi dan Cursor. Pola ini paling terasa gunanya pada operasi multi-langkah yang harus atomik, misalnya transfer saldo:
import os
import sqlite3
if os.path.exists("toko.db"):
os.remove("toko.db")
con = sqlite3.connect("toko.db")
cur = con.cursor()
cur.execute(
"CREATE TABLE rekening (id INTEGER PRIMARY KEY, pemilik TEXT NOT NULL, saldo INTEGER NOT NULL)"
)
cur.executemany(
"INSERT INTO rekening (pemilik, saldo) VALUES (?, ?)",
[("Budi", 50000), ("Siti", 30000)],
)
con.commit()
def transfer(con, dari_id, ke_id, jumlah):
saldo_pengirim = con.execute(
"SELECT saldo FROM rekening WHERE id = ?", (dari_id,)
).fetchone()[0]
if saldo_pengirim < jumlah:
raise ValueError("saldo tidak cukup")
with con:
con.execute(
"UPDATE rekening SET saldo = saldo - ? WHERE id = ?", (jumlah, dari_id)
)
con.execute(
"UPDATE rekening SET saldo = saldo + ? WHERE id = ?", (jumlah, ke_id)
)
transfer(con, 1, 2, 10000)
print("transfer pertama berhasil")
try:
transfer(con, 2, 1, 999999)
except ValueError as e:
print("ditolak:", e)
for pemilik, saldo in con.execute(
"SELECT pemilik, saldo FROM rekening ORDER BY id"
):
print(pemilik, saldo)
con.close()Output yang Dihasilkan:
transfer pertama berhasil
ditolak: saldo tidak cukup
Budi 40000
Siti 40000Mari kita bedah fungsi transfer():
- Baris
saldo_pengirim = con.execute(...).fetchone()[0]: saldo pengirim dibaca dulu sebagai satu angka untuk validasi. - Baris
if saldo_pengirim < jumlah: raise ValueError(...): transfer yang tidak sah ditolak sebelum menyentuh data — exception ini nanti yang memicu rollback di blokwith. - Baris
with con:: keduaUPDATEberjalan di dalam satu transaction.saldo - ?dansaldo + ?harus tereksekusi bersama; jika salah satu gagal, saldo tidak boleh setengah terpotong. - Keluar blok
with: sukses berarti commit, exception berarti rollback — pemanggil fungsi tinggal menangkapValueErrorseperti padatry/exceptdi program utama.
Transfer pertama memindahkan 10000 dari Budi ke Siti. Transfer kedua ditolak karena saldo Siti tidak cukup, dan blok with memastikan tidak ada UPDATE yang setengah jalan — saldo keduanya tetap 40000.
Mode Autocommit dan BEGIN Eksplisit #
Mode bawaan itu praktis, tetapi bukan satu-satunya cara. isolation_level=None menonaktifkan transaction implisit: setiap statement langsung permanen (autocommit), seperti perilaku bawaan sqlite3 CLI yang kita pakai di section Transaksi Dasar. Untuk mengelompokkan perubahan, kita menulis BEGIN sendiri:
import sqlite3
con = sqlite3.connect(":memory:", isolation_level=None)
cur = con.cursor()
cur.execute("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, isi TEXT)")
cur.execute("INSERT INTO pesanan (isi) VALUES (?)", ("kopi gayo",))
print("mode autocommit, setelah INSERT:", con.in_transaction)
cur.execute("BEGIN")
cur.execute("INSERT INTO pesanan (isi) VALUES (?)", ("gula aren",))
print("mode autocommit, setelah BEGIN + INSERT:", con.in_transaction)
cur.execute("SAVEPOINT sebelum_edit")
cur.execute("INSERT INTO pesanan (isi) VALUES (?)", ("salah input",))
cur.execute("ROLLBACK TO sebelum_edit")
cur.execute("RELEASE sebelum_edit")
cur.execute("COMMIT")
print("setelah COMMIT:", con.in_transaction)
print(cur.execute("SELECT isi FROM pesanan ORDER BY id").fetchall())
con.close()Output yang Dihasilkan:
mode autocommit, setelah INSERT: False
mode autocommit, setelah BEGIN + INSERT: True
setelah COMMIT: False
[('kopi gayo',), ('gula aren',)]Perhatikan tiga keadaan berbeda dari output tersebut:
- INSERT tanpa BEGIN:
in_transactiontetapFalse— statement langsung permanen, tidak ada transaction terbuka. BEGINlalu INSERT:in_transactionberubahTrue; sejak titik ini hanyaCOMMITatauROLLBACKyang mengakhiri.SAVEPOINT ... ROLLBACK TO ... RELEASE: savepoint bekerja di dalam transaction — baris “salah input” dibatalkan sebagian tanpa membatalkan “gula aren”. Mekanisme savepoint selengkapnya dibahas pada Chapter Savepoint.
Perbandingan kedua mode tersebut:
| Aspek | Mode bawaan (legacy) | isolation_level=None |
|---|---|---|
BEGIN | otomatis sebelum DML pertama | ditulis manual lewat execute() |
| INSERT tanpa commit | menumpuk di transaction terbuka | langsung permanen |
in_transaction setelah INSERT | True | False |
| Cocok untuk | skrip dan aplikasi sederhana | kontrol penuh, DDL dalam transaction |
Pilih mode bawaan saat kemudahan lebih penting; pilih autocommit saat butuh ketelitian penuh atas kapan transaction dibuka — misalnya membungkus DDL dan DML sekaligus. Dua mode ini melengkapi satu sama lain, dan pemahaman keduanya membuat kita tidak pernah lagi terkejut oleh data yang menguap tanpa jejak.