Memindahkan saldo antar rekening membutuhkan dua UPDATE: saldo pengirim dikurangi, saldo penerima ditambah. Jika proses terputus di antara keduanya, uang hilang begitu saja atau berlipat ganda. Transaksi memecahkan masalah ini dengan mengelompokkan beberapa statement menjadi satu unit kerja: seluruh perubahan terjadi bersama-sama, atau tidak ada sama sekali.

Skema Contoh #

Sepanjang chapter ini kita memakai tabel rekening berikut di berkas toko.db:

sql
CREATE TABLE rekening (
  id INTEGER PRIMARY KEY,
  pemilik TEXT NOT NULL,
  saldo INTEGER NOT NULL CHECK (saldo >= 0)
);

INSERT INTO rekening VALUES (1, 'Budi', 500000), (2, 'Siti', 200000);

Batasan CHECK (saldo >= 0) akan kita manfaatkan untuk memicu galat secara sengaja di bagian akhir.

Menjalankan Transaksi: BEGIN dan COMMIT #

Contoh klasiknya adalah transfer Rp150.000 dari Budi ke Siti:

sql
BEGIN;
UPDATE rekening SET saldo = saldo - 150000 WHERE id = 1;
UPDATE rekening SET saldo = saldo + 150000 WHERE id = 2;
COMMIT;

SELECT id, pemilik, saldo FROM rekening ORDER BY id;

Output yang Dihasilkan:

text
1|Budi|350000
2|Siti|350000

Mari kita bedah pernyataan-pernyataan penting di atas:

  1. Baris 1 (BEGIN): membuka transaksi dan mematikan perilaku autocommit untuk koneksi ini. Semua statement berikutnya dianggap bagian dari satu unit kerja yang sama.
  2. Baris 2-3 (dua UPDATE): engine SQLite menjalankan keduanya, tetapi hasilnya masih bersifat sementara — koneksi lain belum dapat melihatnya.
  3. Baris 4 (COMMIT): menjadikan seluruh perubahan permanen. Titik ini adalah momen “semua atau tidak sama sekali” terjadi.

Membatalkan dengan ROLLBACK #

Sebelum COMMIT, kita bisa membuang seluruh perubahan dengan ROLLBACK. Buka transaksi dan kurangi saldo Budi, lalu sengaja jangan commit:

sql
BEGIN;
UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;

Sekarang buka terminal kedua dan baca tabel yang sama lewat koneksi baru:

bash
sqlite3 toko.db "SELECT id, pemilik, saldo FROM rekening ORDER BY id;"

Output yang Dihasilkan:

text
1|Budi|350000
2|Siti|350000

Koneksi kedua masih melihat saldo lama karena perubahan di koneksi pertama belum di-commit. Namun koneksi yang sama dengan transaksi terbuka bisa melihat keadaan sementaranya sendiri:

sql
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
ROLLBACK;

Output yang Dihasilkan:

text
1|Budi|250000
2|Siti|350000

SELECT di atas dijalankan di dalam transaksi, sehingga saldo Budi tampak sudah berkurang menjadi 250000. ROLLBACK kemudian membuang seluruh perubahan tersebut, dan keadaan kembali seperti semula:

sql
SELECT id, pemilik, saldo FROM rekening ORDER BY id;

Output yang Dihasilkan:

text
1|Budi|350000
2|Siti|350000

Statement Gagal, Transaksi Tetap Terbuka #

Apa yang terjadi jika salah satu statement di tengah transaksi gagal? Mari kita uji dengan menarik saldo jauh melebihi isi rekening:

sql
BEGIN;
UPDATE rekening SET saldo = saldo - 10000 WHERE id = 1;
UPDATE rekening SET saldo = saldo - 999999 WHERE id = 1;

Output yang Dihasilkan:

text
Runtime error: CHECK constraint failed: saldo >= 0 (19)

Statement kedua ditolak karena 340000 dikurangi 999999 menghasilkan saldo negatif. Yang penting diamati: galat ini tidak menutup transaksi. Statement yang gagal dibatalkan sendiri oleh SQLite, tetapi statement pertama tetap berlaku dan transaksi masih terbuka:

sql
SELECT id, pemilik, saldo FROM rekening ORDER BY id;

Output yang Dihasilkan:

text
1|Budi|340000
2|Siti|350000

Karena transaksi masih terbuka, keputusan ada di tangan kita — di sini kita memilih membuang semuanya:

sql
ROLLBACK;
SELECT id, pemilik, saldo FROM rekening ORDER BY id;

Output yang Dihasilkan:

text
1|Budi|350000
2|Siti|350000

DDL pun Bisa Di-rollback #

Berbeda dari MySQL yang melakukan implicit commit setiap kali DDL dijalankan, SQLite memperlakukan CREATE TABLE, DROP TABLE, dan ALTER TABLE sebagai statement biasa di dalam transaksi:

sql
BEGIN;
CREATE TABLE mutasi (
  id INTEGER PRIMARY KEY,
  rekening_id INTEGER NOT NULL,
  keterangan TEXT NOT NULL,
  jumlah INTEGER NOT NULL
);
INSERT INTO mutasi VALUES (1, 1, 'transfer keluar', -150000);
SELECT COUNT(*) FROM mutasi;
ROLLBACK;

Output yang Dihasilkan:

text
1

Di dalam transaksi, tabel mutasi ada dan berisi satu baris. Setelah ROLLBACK, tabelnya ikut lenyap:

bash
sqlite3 toko.db ".tables"

Output yang Dihasilkan:

text
rekening

Transaksi adalah fondasi seluruh jaminan konsistensi di SQLite. Bagaimana engine benar-benar menjamin bahwa unit kerja ini utuh saat listrik mati atau proses dibunuh? Itu bahasan Chapter Memahami ACID berikutnya.