Setelah executeQuery() dijalankan, hasilnya dikemas dalam objek ResultSet — iterator baris hasil query. Pemahaman penuh tentang ResultSet penting karena sebagian besar kode aplikasi database adalah membaca data dari sini. Defaultnya ResultSet hanya bisa bergerak maju (TYPE_FORWARD_ONLY), tetapi JDBC mendukung scrollable cursor yang bisa mundur dan melompat. Pada chapter ini kita akan membedah iterasi dasar, akses kolom, metadata, serta simulasi scrollable cursor di atas SQLite yang tidak mendukungnya secara native.
Iterasi dengan next() #
ResultSet default berada sebelum baris pertama. Method next() memindahkan kursor satu baris ke depan dan mengembalikan Boolean — true jika ada baris berikutnya, false jika sudah melewati baris terakhir. Pola paling umum di Kotlin:
stmt.executeQuery("SELECT id, nama FROM produk").use { rs ->
while (rs.next()) {
val id = rs.getInt("id")
val nama = rs.getString("nama")
println("$id: $nama")
}
}Selalu bungkus ResultSet dengan use { } agar cursor dan resource dilepas saat selesai.
Akses Kolom: Index vs Nama #
Kolom bisa diakses via index 1-based atau via nama:
rs.getString(1) // index 1 = kolom pertama di SELECT
rs.getString("nama") // nama kolom di SELECTIndex sedikit lebih cepat tetapi rentan bug bila urutan kolom di query berubah. Nama lebih jelas dan robust terhadap perubahan urutan — disarankan untuk kode produksi.
Setiap tipe SQL punya method getter yang sesuai: getInt, getLong, getString, getBoolean, getDouble, getDate, getObject. Method getObject berguna untuk tipe yang tidak punya getter spesifik atau saat tipe tidak diketahui saat compile time.
Metadata ResultSet #
Interface ResultSetMetaData memberi tahu jumlah kolom, nama kolom, dan tipe SQL tiap kolom. Berguna untuk utilitas generik seperti exporter tabel ke CSV.
val md = rs.metaData
println("Jumlah kolom: ${md.columnCount}")
for (i in 1..md.columnCount) {
println("Kolom $i: ${md.getColumnName(i)} tipe ${md.getColumnTypeName(i)}")
}Jenis Cursor #
JDBC mendefinisikan tiga jenis cursor:
| Konstanta | Karakteristik |
|---|---|
TYPE_FORWARD_ONLY (default) | Hanya bisa maju, sekali jalan |
TYPE_SCROLL_INSENSITIVE | Bisa mundur dan lompat, tidak melihat perubahan data lain |
TYPE_SCROLL_SENSITIVE | Bisa mundur dan lompat, melihat perubahan data lain |
Method navigasi scrollable: first(), last(), previous(), absolute(n), relative(n), beforeFirst(), afterLast(). Method absolute(3) memindahkan kursor ke baris ke-3; absolute(-1) ke baris terakhir; relative(2) dua baris dari posisi sekarang.
Kode Lengkap: Forward dan Scrollable #
Kode berikut menampilkan iterasi forward-only, akses kolom via nama dan index, baca metadata, dan simulasi scrollable cursor di atas List cache.
// result_sets.kt
// Mendemonstrasikan iterasi ResultSet: next(), getString, getInt, dan
// scrollable ResultSet (TYPE_SCROLL_INSENSITIVE). Memakai SQLite via JDBC.
import java.sql.Connection
import java.sql.DriverManager
import java.sql.ResultSet
import java.sql.Statement
data class Produk(val id: Int, val nama: String, val harga: 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 produk (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT NOT NULL,
harga INTEGER NOT NULL
)"""
)
stmt.executeUpdate(
"""INSERT INTO produk(nama, harga) VALUES
('Kopi Susu', 18000),
('Es Teh Manis', 8000),
('Nasi Goreng', 25000),
('Mie Goreng', 22000),
('Keripik Singkong', 12000)"""
)
}
// 1. Forward-only ResultSet (default): next() untuk iterasi
println("=== Forward-only ResultSet (default) ===")
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT id, nama, harga FROM produk ORDER BY id").use { rs: ResultSet ->
println("Type: ${rs.type} (TYPE_FORWARD_ONLY=1003)")
while (rs.next()) {
// Akses kolom via nama atau index
val id = rs.getInt("id")
val nama = rs.getString("nama")
val harga = rs.getInt("harga")
println(" ${Produk(id, nama, harga)}")
}
}
}
// 2. Akses kolom via index (1-based) vs via nama
println("\n=== Akses kolom: index vs nama ===")
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT nama, harga FROM produk ORDER BY harga DESC LIMIT 3").use { rs ->
println("3 produk termahal:")
while (rs.next()) {
// Index dimulai dari 1 (warisan JDBC dari ODBC)
println(" index: ${rs.getString(1)} - Rp${rs.getInt(2)}")
println(" nama : ${rs.getString("nama")} - Rp${rs.getInt("harga")}")
}
}
}
// 3. Mendapatkan metadata ResultSet
println("\n=== ResultSet metadata ===")
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT id, nama, harga FROM produk LIMIT 1").use { rs ->
val md = rs.metaData
println("Jumlah kolom: ${md.columnCount}")
for (i in 1..md.columnCount) {
println(" Kolom $i: ${md.getColumnName(i)} (${md.getColumnTypeName(i)})")
}
}
}
// 4. Scrollable ResultSet: TYPE_SCROLL_INSENSITIVE
// Catatan: SQLite JDBC hanya mendukung TYPE_FORWARD_ONLY.
// Di bawah ini mensimulasikan API scrollable ResultSet dengan memuat
// semua baris ke List (mirip cara driver Postgres/MySQL menerapkan
// TYPE_SCROLL_INSENSITIVE: cache hasil di sisi client).
println("\n=== Scrollable ResultSet (simulasi via List cache) ===")
val rows = mutableListOf<Produk>()
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT id, nama, harga FROM produk ORDER BY id").use { rs ->
while (rs.next()) {
rows.add(Produk(rs.getInt("id"), rs.getString("nama"), rs.getInt("harga")))
}
}
}
val scroll = ScrollableCursor(rows)
println("Cursor awal: posisi=${scroll.row}, isFirst=${scroll.isFirst()}")
scroll.last()
println("Setelah last(): row=${scroll.row}, data=${scroll.current()}")
scroll.first()
println("Setelah first(): row=${scroll.row}, data=${scroll.current()}")
scroll.absolute(3)
println("Setelah absolute(3): row=${scroll.row}, data=${scroll.current()}")
scroll.previous()
println("Setelah previous(): row=${scroll.row}, data=${scroll.current()}")
scroll.relative(2)
println("Setelah relative(2): row=${scroll.row}, data=${scroll.current()}")
println("\n=== Updateable ResultSet (simulasi) ===")
// Updateable ResultSet memerlukan primary key dan CONCUR_UPDATABLE.
// SQLite JDBC tidak mendukung CONCUR_UPDATABLE; simulasi via UPDATE biasa.
val hargaLama = scroll.first().harga
conn.prepareStatement("UPDATE produk SET harga = ? WHERE id = 1").use { ps ->
ps.setInt(1, hargaLama + 1000)
ps.executeUpdate()
}
println("Update row 1: harga $hargaLama -> ${hargaLama + 1000}")
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT id, nama, harga FROM produk WHERE id = 1").use { rs ->
if (rs.next()) println("Verifikasi: ${Produk(rs.getInt("id"), rs.getString("nama"), rs.getInt("harga"))}")
}
}
// 6. Ringkasan jenis cursor
println("\n=== Ringkasan jenis cursor ===")
println("| Type | Konkurensi | Keterangan |")
println("|----------------------------|------------|-------------------------------|")
println("| TYPE_FORWARD_ONLY (1003) | Default | Sekali jalan, next() saja |")
println("| TYPE_SCROLL_INSENSITIVE | Read-only | Bisa mundur, tak lihat update |")
println("| TYPE_SCROLL_SENSITIVE | Read-only | Bisa mundur, lihat update |")
}
keeper.close()
}
// Simulasi cursor scrollable ResultSet (mirip implementasi driver Postgres
// yang memuat seluruh hasil query ke client ketika type=SCROLL_INSENSITIVE).
class ScrollableCursor<T>(private val rows: List<T>) {
private var cursor = -1 // posisi sebelum baris pertama
val row: Int get() = cursor + 1 // 1-based, seperti ResultSet.row
fun isFirst(): Boolean = cursor == 0
fun isLast(): Boolean = cursor == rows.size - 1
fun current(): T = rows[cursor]
fun first(): T { cursor = 0; return current() }
fun last(): T { cursor = rows.size - 1; return current() }
fun next(): Boolean {
if (cursor < rows.size - 1) { cursor++; return true }
return false
}
fun previous(): Boolean {
if (cursor > 0) { cursor--; return true }
return false
}
fun absolute(pos: Int): T {
cursor = when {
pos > 0 -> (pos - 1).coerceAtMost(rows.size - 1)
pos < 0 -> (rows.size + pos).coerceAtLeast(0)
else -> -1 // 0 = before first
}
return current()
}
fun relative(offset: Int): T {
cursor = (cursor + offset).coerceIn(0, rows.size - 1)
return current()
}
}Output yang Dihasilkan:
=== Forward-only ResultSet (default) ===
Type: 1003 (TYPE_FORWARD_ONLY=1003)
Produk(id=1, nama=Kopi Susu, harga=18000)
Produk(id=2, nama=Es Teh Manis, harga=8000)
Produk(id=3, nama=Nasi Goreng, harga=25000)
Produk(id=4, nama=Mie Goreng, harga=22000)
Produk(id=5, nama=Keripik Singkong, harga=12000)
=== Akses kolom: index vs nama ===
3 produk termahal:
index: Nasi Goreng - Rp25000
nama : Nasi Goreng - Rp25000
index: Mie Goreng - Rp22000
nama : Mie Goreng - Rp22000
index: Kopi Susu - Rp18000
nama : Kopi Susu - Rp18000
=== ResultSet metadata ===
Jumlah kolom: 3
Kolom 1: id (INTEGER)
Kolom 2: nama (TEXT)
Kolom 3: harga (INTEGER)
=== Scrollable ResultSet (simulasi via List cache) ===
Cursor awal: posisi=0, isFirst=false
Setelah last(): row=5, data=Produk(id=5, nama=Keripik Singkong, harga=12000)
Setelah first(): row=1, data=Produk(id=1, nama=Kopi Susu, harga=18000)
Setelah absolute(3): row=3, data=Produk(id=3, nama=Nasi Goreng, harga=25000)
Setelah previous(): row=2, data=Produk(id=2, nama=Es Teh Manis, harga=8000)
Setelah relative(2): row=4, data=Produk(id=4, nama=Mie Goreng, harga=22000)
=== Updateable ResultSet (simulasi) ===
Update row 1: harga 18000 -> 19000
Verifikasi: Produk(id=1, nama=Kopi Susu, harga=19000)
=== Ringkasan jenis cursor ===
| Type | Konkurensi | Keterangan |
|----------------------------|------------|-------------------------------|
| TYPE_FORWARD_ONLY (1003) | Default | Sekali jalan, next() saja |
| TYPE_SCROLL_INSENSITIVE | Read-only | Bisa mundur, tak lihat update |
| TYPE_SCROLL_SENSITIVE | Read-only | Bisa mundur, lihat update |Updateable ResultSet #
Selain scrollable, JDBC mendefinisikan CONCUR_UPDATABLE yang mengizinkan update baris langsung melalui ResultSet — rs.updateInt("harga", 19000); rs.updateRow(). Fitur ini memerlukan primary key di query dan tidak semua driver mendukungnya. Di SQLite JDBC, fitur ini tidak tersedia; pola yang lebih portable adalah membaca ResultSet, lalu menjalankan UPDATE terpisah via PreparedStatement, seperti simulasi pada kode di atas.
Referensi Lanjutan #
- java.sql package (Java SE 21)
— API reference
ResultSetuntuk iterasi hasil query di paketjava.sql
Selanjutnya #
Sekarang kita bisa membaca dan menulis data via JDBC. Tetapi operasi nyata sering melibatkan beberapa perubahan yang harus atomik — misalnya transfer uang antar akun. Chapter Transaction Management akan membahas setAutoCommit(false), commit(), rollback(), serta konsep ACID.