ROLLBACK bersifat serba atau bukan sama sekali: seluruh perubahan sejak BEGIN dibuang, tidak peduli betapa panjang dan rumit transaksinya. Sering kali kita hanya ingin membatalkan sebagian — misalnya transfer tetap berjalan, tetapi pencatatan audit-nya diulang. Savepoint mengatasi ini dengan menandai titik pulih di dalam transaksi, sehingga kita bisa kembali ke titik mana pun tanpa membuang sisanya. Chapter ini membahas tiga perintahnya (SAVEPOINT, ROLLBACK TO, RELEASE), savepoint bersarang, sampai pola impor data dengan pemulihan per batch.
Checkpoint di Dalam Transaksi #
Skema contoh kita memakai kembali tabel rekening dari Chapter Transaksi Dasar: BEGIN, COMMIT, ROLLBACK, ditambah tabel mutasi untuk mencatat riwayat:
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);
CREATE TABLE mutasi (
id INTEGER PRIMARY KEY,
rekening_id INTEGER NOT NULL,
keterangan TEXT NOT NULL,
jumlah INTEGER NOT NULL
);Kecuali disebutkan lain, setiap demo di chapter ini dimulai dari keadaan awal di atas — buat ulang toko.db dari skema tersebut sebelum mengulang sebuah demo bila ingin keluaran yang sama.
Skenarionya untuk demo pertama: transfer Rp100.000 dari Budi ke Siti sudah selesai, lalu kita mencatat dua baris audit di dalam transaksi yang sama — dan di tengah jalan memutuskan catatan itu tidak jadi dipakai:
BEGIN;
UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;
UPDATE rekening SET saldo = saldo + 100000 WHERE id = 2;
SAVEPOINT transfer_ok;
INSERT INTO mutasi VALUES (1, 1, 'transfer keluar', -100000);
INSERT INTO mutasi VALUES (2, 2, 'transfer masuk', 100000);
SELECT COUNT(*) FROM mutasi;
ROLLBACK TO transfer_ok;
SELECT COUNT(*) FROM mutasi;
COMMIT;
SELECT id, pemilik, saldo FROM rekening ORDER BY id;Output yang Dihasilkan:
2
0
1|Budi|400000
2|Siti|300000Mari kita bedah pernyataan-pernyataan penting di atas:
- Baris 4 (
SAVEPOINT transfer_ok): menandai checkpoint bernamatransfer_okpada keadaan setelah keduaUPDATEselesai. Nama savepoint bebas, dengan aturan yang sama seperti nama tabel. - Baris 5-6 (dua
INSERTkemutasi): mencatat audit transfer — dua baris masuk, terbukti dariCOUNT(*)yang mengembalikan2. - Baris 8 (
ROLLBACK TO transfer_ok): membatalkan semua perubahan setelah checkpoint — kedua barismutasilenyap (COUNT(*)kembali0) — tetapi keduaUPDATEtransfer tetap utuh. - Baris 10 (
COMMIT): mengunci keadaan akhir: transfer tersimpan, audit dibuang.
Bedanya dengan ROLLBACK polos tampak jelas: ROLLBACK TO hanya memundurkan transaksi ke titik tertentu, lalu transaksi tetap terbuka dan bisa dilanjutkan.
Savepoint Bersarang #
Savepoint bisa dibuat bertingkat, dan perilakunya seperti tumpukan checkpoint. Kita buat savepoint luar, lalu savepoint di dalamnya:
BEGIN;
SAVEPOINT luar;
UPDATE rekening SET saldo = saldo - 50000 WHERE id = 1;
SAVEPOINT dalam;
UPDATE rekening SET saldo = saldo - 25000 WHERE id = 1;
SELECT saldo FROM rekening WHERE id = 1;
ROLLBACK TO dalam;
SELECT saldo FROM rekening WHERE id = 1;
UPDATE rekening SET saldo = saldo - 25000 WHERE id = 1;
SELECT saldo FROM rekening WHERE id = 1;
ROLLBACK TO luar;
SELECT saldo FROM rekening WHERE id = 1;
COMMIT;Output yang Dihasilkan:
425000
450000
425000
500000Uraiannya sebagai berikut:
ROLLBACK TO dalam: hanya membatalkanUPDATEsebesar 25000 setelah checkpointdalam— saldo kembali ke 450000, sementara potongan 50000 setelahluartetap berlaku.UPDATEulang: potongan 25000 diulang, saldo turun lagi ke 425000. Ini membuktikanROLLBACK TOtidak menghapus savepoint-nya; savepointdalammasih bisa dipakai lagi.ROLLBACK TO luar: memundurkan ke checkpoint paling awal, sehingga seluruh potongan — termasuk yang diulang barusan — terhapus sekaligus. Saldo kembali ke 500000.
Sementara ROLLBACK TO memundurkan keadaan, RELEASE SAVEPOINT justru melepas checkpoint dan menganggap perubahan setelahnya final. Melepas savepoint luar akan melepas semua savepoint di dalamnya sekaligus:
BEGIN;
SAVEPOINT luar;
UPDATE rekening SET saldo = saldo - 50000 WHERE id = 1;
SAVEPOINT dalam;
UPDATE rekening SET saldo = saldo - 25000 WHERE id = 1;
RELEASE SAVEPOINT luar;
SELECT saldo FROM rekening WHERE id = 1;
COMMIT;Output yang Dihasilkan:
425000Setelah RELEASE SAVEPOINT luar, kedua potongan digabung menjadi bagian resmi transaksi dan COMMIT mengabadikannya — koneksi baru akan menemukan saldo 425000. Nama dalam ikut lenyap bersama releasenya; perintah ROLLBACK TO dalam setelah itu ditolak dengan galat no such savepoint: dalam.
Savepoint Tanpa BEGIN #
Perilaku yang sering mengejutkan: SAVEPOINT di luar transaksi apa pun tetap sah, dan engine SQLite diam-diam membuka transaksi untuknya. Saat savepoint terluar di-RELEASE, transaksi implisit itu di-commit — mulai lagi dari keadaan awal:
sqlite3 toko.db "SAVEPOINT sesi_koreksi; UPDATE rekening SET saldo = saldo - 10000 WHERE id = 1; RELEASE SAVEPOINT sesi_koreksi;"
sqlite3 toko.db "SELECT id, pemilik, saldo FROM rekening ORDER BY id;"Output yang Dihasilkan:
1|Budi|490000
2|Siti|200000Perubahan tersimpan permanen padahal tidak ada BEGIN maupun COMMIT yang kita ketik. Sebaliknya, bila koneksi berakhir tanpa RELEASE, transaksi implisit itu dibatalkan — uji berikut melanjutkan keadaan demo di atas, dengan saldo Budi 490000:
echo "SAVEPOINT percobaan; UPDATE rekening SET saldo = saldo - 10000 WHERE id = 1;" | sqlite3 toko.db
sqlite3 toko.db "SELECT id, pemilik, saldo FROM rekening ORDER BY id;"Output yang Dihasilkan:
1|Budi|490000
2|Siti|200000Pola Impor dengan Pemulihan per Batch #
Kegunaan paling praktis dari savepoint adalah impor data besar: satu baris rusak tidak perlu membatalkan seluruh impor. Idenya, setiap batch dibungkus savepoint sendiri — batch yang gagal dibatalkan batch-nya saja, impor berlanjut. Ujinya memakai berkas baru impor.db berisi satu tabel dengan pemeriksaan harga (jalankan CREATE TABLE berikut untuk membuatnya):
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
harga INTEGER NOT NULL CHECK (harga >= 0)
);Simpan skrip impor berikut sebagai impor.sql di folder kerja Anda, lalu jalankan dengan .read:
sqlite3 impor.db ".read impor.sql"BEGIN;
SAVEPOINT batch_1;
INSERT INTO produk VALUES (1, 'Kopi Gayo 200g', 45000);
INSERT INTO produk VALUES (2, 'Gula Aren 500g', 30000);
RELEASE SAVEPOINT batch_1;
SAVEPOINT batch_2;
INSERT INTO produk VALUES (3, 'Teh Melati 25 sachet', 15000);
INSERT INTO produk VALUES (4, 'Produk Rusak', -5000);
ROLLBACK TO batch_2;
RELEASE SAVEPOINT batch_2;
SAVEPOINT batch_3;
INSERT INTO produk VALUES (5, 'Keripik Singkong 100g', 12000);
RELEASE SAVEPOINT batch_3;
COMMIT;
SELECT COUNT(*) FROM produk;Output yang Dihasilkan:
Runtime error near line 8: CHECK constraint failed: harga >= 0 (19)
3Baris kedelapan (INSERT harga negatif untuk Produk Rusak) ditolak constraint, dan CLI melanjutkan ke baris setelahnya — pesan near line 8 menunjuk baris kedelapan berkas impor.sql. ROLLBACK TO batch_2 kemudian membuang seluruh batch kedua, termasuk baris Teh Melati yang sebenarnya valid. Hasil akhirnya terlihat dari koneksi baru:
sqlite3 impor.db "SELECT id, nama, harga FROM produk ORDER BY id;"Output yang Dihasilkan:
1|Kopi Gayo 200g|45000
2|Gula Aren 500g|30000
5|Keripik Singkong 100g|12000Satu catatan penutup: transaksi yang memuat banyak savepoint tetap satu transaksi penuh di mata sistem locking — sejak UPDATE pertama hingga COMMIT, koneksi kita memegang lock penulis seperti dibahas pada Chapter Isolation dan Locking. Chapter berikutnya, Journal Modes dan WAL, membedah apa yang sebenarnya ditulis engine SQLite ke disk selama transaksi itu berlangsung.