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:

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

text
setelah CREATE TABLE: False
setelah INSERT: True
setelah rollback: False
jumlah baris: 0
setelah commit: False
jumlah baris: 1

Mari kita ikuti alurnya:

  1. Baris 11 (print("setelah CREATE TABLE: ...")): CREATE TABLE berjalan dan langsung selesai — in_transaction bernilai False karena DDL tidak membuka transaction implisit.
  2. Baris 12 (cur.execute("INSERT ...")): modul sqlite3 diam-diam menjalankan BEGIN lebih dulu, baru statement INSERT — in_transaction kini True.
  3. Baris 15 (con.rollback()): seluruh perubahan sejak BEGIN dibatalkan, termasuk INSERT kopi gayo; jumlah baris kembali 0.
  4. Baris 19-20 (INSERT lalu con.commit()): INSERT gula aren masuk transaction baru, lalu commit() membuatnya permanen — jumlah baris 1 dan in_transaction kembali False.

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:

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

text
transfer pertama berhasil
ditolak: saldo tidak cukup
Budi 40000
Siti 40000

Mari kita bedah fungsi transfer():

  1. Baris saldo_pengirim = con.execute(...).fetchone()[0]: saldo pengirim dibaca dulu sebagai satu angka untuk validasi.
  2. Baris if saldo_pengirim < jumlah: raise ValueError(...): transfer yang tidak sah ditolak sebelum menyentuh data — exception ini nanti yang memicu rollback di blok with.
  3. Baris with con:: kedua UPDATE berjalan di dalam satu transaction. saldo - ? dan saldo + ? harus tereksekusi bersama; jika salah satu gagal, saldo tidak boleh setengah terpotong.
  4. Keluar blok with: sukses berarti commit, exception berarti rollback — pemanggil fungsi tinggal menangkap ValueError seperti pada try/except di 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:

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

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

  1. INSERT tanpa BEGIN: in_transaction tetap False — statement langsung permanen, tidak ada transaction terbuka.
  2. BEGIN lalu INSERT: in_transaction berubah True; sejak titik ini hanya COMMIT atau ROLLBACK yang mengakhiri.
  3. 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:

AspekMode bawaan (legacy)isolation_level=None
BEGINotomatis sebelum DML pertamaditulis manual lewat execute()
INSERT tanpa commitmenumpuk di transaction terbukalangsung permanen
in_transaction setelah INSERTTrueFalse
Cocok untukskrip dan aplikasi sederhanakontrol 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.