Banyak operasi database nyata harus atomik — semua berhasil atau semua gagal. Transfer uang antar akun, pemesanan dengan stok terkurangi, dan pembuatan pesanan beserta item-itemnya adalah contoh operasi yang tidak boleh setengah jalan. JDBC mengelola hal ini melalui transaction dengan setAutoCommit(false), commit(), dan rollback(). Pada chapter ini kita akan membedah properti ACID, transaksi manual, savepoint untuk partial rollback, dan isolation levels yang menentukan seberapa terisolasi transaksi paralel.
Auto-Commit Default #
Secara default, JDBC berjalan dalam mode auto-commit — setiap Statement dieksekusi sebagai transaksi sendiri yang langsung di- commit. Untuk operasi sederhana ini cukup, tetapi ketika dua UPDATE harus berjalan bersamaan, kita perlu mematikan auto-commit.
conn.createStatement().use { stmt ->
stmt.executeUpdate("UPDATE akun SET saldo = saldo - 200 WHERE id = 1") // commit 1
stmt.executeUpdate("UPDATE akun SET saldo = saldo + 200 WHERE id = 2") // commit 2
}
// Bila proses crash di antara dua statement, saldo Budi berkurang tapi Siti belum bertambah.Transaksi Manual #
Untuk transaksi manual, panggil conn.autoCommit = false, jalankan SQL, lalu panggil conn.commit() jika semua sukses atau conn.rollback() bila ada error.
conn.autoCommit = false
try {
// ... eksekusi beberapa SQL ...
conn.commit()
} catch (e: Exception) {
conn.rollback()
throw e
} finally {
conn.autoCommit = true
}Properti ACID #
- Atomicity: semua operasi dalam transaksi berhasil atau semua dibatalkan. Tidak ada kondisi setengah berhasil.
- Consistency: transaksi membawa database dari satu state valid ke state valid lain. Constraint (FK, unique, check) tetap dipatuhi.
- Isolation: transaksi paralel tidak saling mengganggu — efeknya seperti dijalankan serial.
- Durability: perubahan yang sudah di- commit permanen walau sistem mati mendadak.
Savepoint untuk Partial Rollback #
Kadang kita ingin rollback sebagian transaksi, bukan seluruhnya. Savepoint memungkinkan hal ini: tandai posisi di tengah transaksi, lalu rollback(savepoint) untuk membatalkan hanya operasi setelah posisi tersebut.
conn.autoCommit = false
try {
stmt.executeUpdate("INSERT INTO log_audit VALUES ('Mulai transfer')")
val sp = conn.setSavepoint("BEFORE_TRANSFER")
try {
stmt.executeUpdate("UPDATE akun SET saldo = saldo - 100000 WHERE id = 1")
// ... cek saldo negatif -> throw
conn.commit()
} catch (e: Exception) {
conn.rollback(sp) // hanya transfer yang dibatalkan, log tetap ada
conn.commit()
}
} finally { conn.autoCommit = true }Isolation Levels #
Isolation level menentukan fenomena apa saja yang mungkin terjadi saat dua transaksi paralel:
| Level | Dirty Read | Non-repeatable Read | Phantom |
|---|---|---|---|
| READ_UNCOMMITTED | Ya | Ya | Ya |
| READ_COMMITTED | Tidak | Ya | Ya |
| REPEATABLE_READ | Tidak | Tidak | Ya |
| SERIALIZABLE | Tidak | Tidak | Tidak |
- Dirty read: membaca perubahan yang belum di- commit transaksi lain.
- Non-repeatable read: membaca ulang baris yang sama memberi hasil berbeda.
- Phantom read: query
WHEREyang sama memberi jumlah baris berbeda.
Level lebih tinggi lebih aman tetapi lebih lambat karena lebih banyak locking. PostgreSQL default READ_COMMITTED; MySQL InnoDB default REPEATABLE_READ.
Kode Lengkap: Transfer Uang dengan ACID #
Kode berikut mensimulasikan transfer uang antar akun. Pertama berhasil, lalu ada kasus gagal di tengah yang di- rollback, dan akhirnya demo savepoint.
// transaction_management.kt
// Mendemonstrasikan transaction management dengan JDBC: setAutoCommit(false),
// commit(), rollback(), savepoint, dan ACID properties. Memakai SQLite via JDBC.
import java.sql.Connection
import java.sql.DriverManager
data class Akun(val id: Int, val nama: String, var saldo: Int)
fun main() {
Class.forName("org.sqlite.JDBC")
val url = "jdbc:sqlite:file::memory:?cache=shared"
val keeper: Connection = DriverManager.getConnection(url)
DriverManager.getConnection(url).use { conn ->
conn.createStatement().use { stmt ->
stmt.execute(
"""CREATE TABLE akun (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
saldo INTEGER NOT NULL
)"""
)
stmt.executeUpdate("INSERT INTO akun VALUES (1, 'Budi', 1000)")
stmt.executeUpdate("INSERT INTO akun VALUES (2, 'Siti', 500)")
}
println("=== ACID Properties ===")
println("Atomicity: semua operasi dalam transaksi berhasil atau semua rollback")
println("Consistency: transaksi membawa DB dari satu state valid ke state valid lain")
println("Isolation: transaksi paralel seolah-olah dijalankan berurutan")
println("Durability: perubahan yang sudah commit permanen walau sistem crash")
// 1. Default: auto-commit ON, setiap statement = 1 transaksi
println("\n=== Default: auto-commit = true ===")
println("autoCommit: ${conn.autoCommit}")
println("Setiap UPDATE/INSERT/DELETE otomatis di-commit setelah eksekusi.")
// 2. Manual transaction: setAutoCommit(false) -> commit() atau rollback()
println("\n=== Manual transaction: transfer uang ===")
conn.autoCommit = false
try {
conn.prepareStatement("UPDATE akun SET saldo = saldo - ? WHERE id = ?").use { ps ->
ps.setInt(1, 200); ps.setInt(2, 1); ps.executeUpdate()
}
println("Debit Budi -200 (belum commit)")
conn.prepareStatement("UPDATE akun SET saldo = saldo + ? WHERE id = ?").use { ps ->
ps.setInt(1, 200); ps.setInt(2, 2); ps.executeUpdate()
}
println("Credit Siti +200 (belum commit)")
conn.commit()
println("commit() dipanggil, perubahan permanen")
} catch (e: Exception) {
conn.rollback()
println("rollback() dipanggil karena error: ${e.message}")
}
conn.autoCommit = true
// Verifikasi
println("Hasil setelah transfer:")
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT id, nama, saldo FROM akun ORDER BY id").use { rs ->
while (rs.next()) {
println(" Akun(id=${rs.getInt("id")}, nama=${rs.getString("nama")}, saldo=${rs.getInt("saldo")})")
}
}
}
// 3. Rollback: simulasi kegagalan
println("\n=== Rollback: simulasi gagal di tengah ===")
conn.autoCommit = false
try {
conn.prepareStatement("UPDATE akun SET saldo = saldo - ? WHERE id = ?").use { ps ->
ps.setInt(1, 1000); ps.setInt(2, 2); ps.executeUpdate()
}
println("Debit Siti -1000 (belum commit)")
// Simulasi error: cek saldo penerima
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT saldo FROM akun WHERE id = 1").use { rs ->
if (rs.next() && rs.getInt(1) < 0) {
throw IllegalStateException("Saldo penerima negatif, tidak boleh")
}
}
}
// Force error
throw IllegalStateException("Simulasi error: bank service offline")
} catch (e: Exception) {
conn.rollback()
println("rollback() dipanggil: ${e.message}")
}
conn.autoCommit = true
println("Hasil setelah rollback (saldo Siti kembali):")
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT id, nama, saldo FROM akun ORDER BY id").use { rs ->
while (rs.next()) {
println(" Akun(id=${rs.getInt("id")}, nama=${rs.getString("nama")}, saldo=${rs.getInt("saldo")})")
}
}
}
// 4. Savepoint: rollback sebagian transaksi
println("\n=== Savepoint: rollback sebagian ===")
conn.autoCommit = false
try {
// Step 1: insert log entry
conn.createStatement().use { it.execute("CREATE TABLE IF NOT EXISTS log_audit (msg TEXT)") }
conn.createStatement().use { it.executeUpdate("INSERT INTO log_audit VALUES ('Transfer dimulai')") }
val savepoint = conn.setSavepoint("BEFORE_TRANSFER")
// Step 2: transfer (gagal simulasi)
try {
conn.prepareStatement("UPDATE akun SET saldo = saldo - ? WHERE id = ?").use { ps ->
ps.setInt(1, 100000); ps.setInt(2, 1); ps.executeUpdate()
}
// Saldo seharusnya negatif -> rollback to savepoint
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT saldo FROM akun WHERE id = 1").use { rs ->
if (rs.next() && rs.getInt(1) < 0) throw IllegalStateException("Saldo negatif")
}
}
conn.commit()
} catch (e: Exception) {
// Rollback hanya transfer, log tetap disimpan
conn.rollback(savepoint)
println("Rollback to savepoint '${savepoint.savepointId ?: savepoint.savepointName}': ${e.message}")
conn.commit() // commit log_audit
}
} finally {
conn.autoCommit = true
}
println("Log audit tetap tersimpan walau transfer di-rollback:")
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT msg FROM log_audit").use { rs ->
while (rs.next()) println(" log: ${rs.getString("msg")}")
}
}
// 5. Isolation levels
println("\n=== Isolation Levels (JDBC) ===")
println("| Level | Konstanta | Dirty Read | Non-repeatable | Phantom |")
println("|--------------------|----------------------------|------------|-----------------|---------|")
println("| READ_UNCOMMITTED | TRANSACTION_READ_UNCOMMITTED | Ya | Ya | Ya |")
println("| READ_COMMITTED | TRANSACTION_READ_COMMITTED | Tidak | Ya | Ya |")
println("| REPEATABLE_READ | TRANSACTION_REPEATABLE_READ | Tidak | Tidak | Ya |")
println("| SERIALIZABLE | TRANSACTION_SERIALIZABLE | Tidak | Tidak | Tidak |")
}
keeper.close()
}Output yang Dihasilkan:
=== ACID Properties ===
Atomicity: semua operasi dalam transaksi berhasil atau semua rollback
Consistency: transaksi membawa DB dari satu state valid ke state valid lain
Isolation: transaksi paralel seolah-olah dijalankan berurutan
Durability: perubahan yang sudah commit permanen walau sistem crash
=== Default: auto-commit = true ===
autoCommit: true
Setiap UPDATE/INSERT/DELETE otomatis di-commit setelah eksekusi.
=== Manual transaction: transfer uang ===
Debit Budi -200 (belum commit)
Credit Siti +200 (belum commit)
commit() dipanggil, perubahan permanen
Hasil setelah transfer:
Akun(id=1, nama=Budi, saldo=800)
Akun(id=2, nama=Siti, saldo=700)
=== Rollback: simulasi gagal di tengah ===
Debit Siti -1000 (belum commit)
rollback() dipanggil: Simulasi error: bank service offline
Hasil setelah rollback (saldo Siti kembali):
Akun(id=1, nama=Budi, saldo=800)
Akun(id=2, nama=Siti, saldo=700)
=== Savepoint: rollback sebagian ===
Rollback to savepoint '1': Saldo negatif
Log audit tetap tersimpan walau transfer di-rollback:
log: Transfer dimulai
=== Isolation Levels (JDBC) ===
| Level | Konstanta | Dirty Read | Non-repeatable | Phantom |
|--------------------|----------------------------|------------|-----------------|---------|
| READ_UNCOMMITTED | TRANSACTION_READ_UNCOMMITTED | Ya | Ya | Ya |
| READ_COMMITTED | TRANSACTION_READ_COMMITTED | Tidak | Ya | Ya |
| REPEATABLE_READ | TRANSACTION_REPEATABLE_READ | Tidak | Tidak | Ya |
| SERIALIZABLE | TRANSACTION_SERIALIZABLE | Tidak | Tidak | Tidak |Mari kita amati bagian-bagian kunci:
conn.autoCommit = false— setelah baris ini, setiapexecuteUpdatemenjadi bagian dari transaksi yang sama hinggacommit()ataurollback()dipanggil.- Transfer berhasil: dua
UPDATEdijalankan, lalucommit()membuat perubahan permanen. Saldo Budi turun 200, saldo Siti naik 200. - Transfer gagal di tengah:
UPDATEpertama menurunkan saldo Siti 1000, lalu exception dilempar.rollback()di blokcatchmembatalkan kedua perubahan (saldo Siti kembali ke 300). - Savepoint: log audit dimasukkan, savepoint
BEFORE_TRANSFERditandai. Transfer gagal karena saldo Budi jadi negatif.rollback(savepoint)hanya membatalkanUPDATEakun, sementara log tetap ada dan di- commit.
Referensi Lanjutan #
- java.sql package (Java SE 21)
— API reference
Connection.setAutoCommit,commit, danrollbackdi paketjava.sql
Selanjutnya #
Membuka dan menutup transaksi setiap request sangat mahal karena membuat koneksi TCP baru. Chapter Connection Pooling akan memperkenalkan HikariCP yang memungkinkan koneksi dipakai ulang antar request.