Setelah memahami konsep relational database, langkah berikutnya adalah mengakses database dari kode Kotlin. JDBC (Java Database Connectivity) adalah API standar Java untuk berbicara dengan RDBMS mana pun — PostgreSQL, MySQL, SQLite, atau Oracle. Kotlin dapat memakai JDBC langsung karena berjalan di atas JVM. Pada chapter ini kita akan membongkar empat interface inti JDBC — DriverManager, Connection, Statement, dan ResultSet — dan menampilkan wrapper Kotlin berbentuk extension function agar kode terasa lebih idiomatis.

Hierarki JDBC API #

JDBC menyusun fungsinya dalam hierarki: DriverManager membuat Connection, Connection membuat Statement, dan Statement mengeksekusi SQL yang mengembalikan ResultSet. Setiap objek di rantai ini sebaiknya ditutup setelah dipakai agar resource (socket TCP, cursor DB, memori) kembali ke sistem. Pola yang direkomendasikan di Kotlin adalah use { ... } yang otomatis memanggil close() di akhir blok, mirip try-with-resources di Java.

text
DriverManager  ->  Connection  ->  Statement  ->  ResultSet
   (factory)       (sesi DB)       (eksekutor)     (iterator baris)

DriverManager dan Connection #

DriverManager.getConnection(url) memilih driver JDBC yang cocok berdasarkan URL dan membuka koneksi baru ke database. URL JDBC berbentuk jdbc:<subprotocol>:<subname>. Untuk SQLite, URL-nya jdbc:sqlite::memory:; untuk PostgreSQL, jdbc:postgresql://host:5432/db.

Sejak JDBC 4.0, pemanggilan Class.forName("org.sqlite.JDBC") tidak wajib bila driver sudah mendaftarkan diri via META-INF/services/java.sql.Driver. Tetap menuliskannya tetap aman dan eksplisit, terutama saat belajar.

Statement dan ResultSet #

Statement adalah antarmuka untuk mengeksekusi SQL. Tiga method utamanya:

  • execute(String sql) — generik, mengembalikan Boolean (true jika ada ResultSet).
  • executeQuery(String sql) — khusus SELECT, langsung mengembalikan ResultSet.
  • executeUpdate(String sql) — khusus INSERT/UPDATE/DELETE/DDL, mengembalikan jumlah baris terdampak.

ResultSet berperilaku seperti iterator satu arah: awalnya kursor berada sebelum baris pertama. Method next() memajukan kursor satu baris dan mengembalikan Boolean (false jika sudah di akhir). Kolom diakses via getString("nama") atau getInt(1) (1-based index, warisan dari ODBC).

Kode Lengkap: Hierarki JDBC di Aksi #

Kode berikut menunjukkan penggunaan penuh hierarki JDBC dengan SQLite in-memory. Kita membuat tabel, menyisipkan data, kemudian membacanya kembali.

kotlin
// jdbc_fundamentals.kt
// Mengenal JDBC API: DriverManager, Connection, Statement, ResultSet.
// Memakai SQLite via JDBC agar bisa dijalankan tanpa server DB.

import java.sql.Connection
import java.sql.DriverManager
import java.sql.ResultSet
import java.sql.Statement

fun main() {
    Class.forName("org.sqlite.JDBC")
    // SQLite in-memory shared agar tabel persisten antar koneksi dalam satu proses
    val url = "jdbc:sqlite:file::memory:?cache=shared"
    // Keeper: menjaga database in-memory tetap hidup selama proses berjalan
    val keeper: Connection = DriverManager.getConnection(url)

    // Connection: sesi komunikasi dengan DB
    DriverManager.getConnection(url).use { conn ->
        println("=== Connection ===")
        println("Auto-commit default: ${conn.autoCommit}")

        // Statement: eksekusi SQL tanpa parameter
        conn.createStatement().use { stmt: Statement ->
            // executeUpdate untuk DDL
            stmt.executeUpdate(
                """CREATE TABLE produk (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    nama TEXT NOT NULL,
                    harga INTEGER NOT NULL
                )"""
            )
            println("\n=== Statement.executeUpdate() (DDL) ===")
            println("Tabel 'produk' dibuat")

            // executeUpdate untuk DML INSERT
            val rowsInserted = stmt.executeUpdate(
                """INSERT INTO produk(nama, harga) VALUES
                   ('Kopi Susu', 18000),
                   ('Es Teh Manis', 8000),
                   ('Nasi Goreng', 25000)"""
            )
            println("\n=== Statement.executeUpdate() (INSERT) ===")
            println("Rows inserted: $rowsInserted")

            // executeQuery untuk SELECT, mengembalikan ResultSet
            stmt.executeQuery("SELECT id, nama, harga FROM produk ORDER BY id").use { rs: ResultSet ->
                println("\n=== Statement.executeQuery() (SELECT) ===")
                println("id | nama            | harga")
                println("---+-----------------+------")
                while (rs.next()) {
                    val id = rs.getInt("id")
                    val nama = rs.getString("nama")
                    val harga = rs.getInt("harga")
                    println("%2d | %-15s | %d".format(id, nama, harga))
                }
            }

            // execute() untuk operasi yang tidak diketahui hasilnya
            val hasResultSet = stmt.execute("SELECT COUNT(*) AS total FROM produk")
            println("\n=== Statement.execute() ===")
            println("Has ResultSet: $hasResultSet")
            if (hasResultSet) {
                stmt.resultSet.use { rs ->
                    if (rs.next()) println("Total baris: ${rs.getInt("total")}")
                }
            }
        }
    }

    println("\n=== Hierarki JDBC API ===")
    println("DriverManager -> Connection -> Statement -> ResultSet")
    println("  - DriverManager: factory untuk Connection berdasarkan URL")
    println("  - Connection: sesi ke database, mengatur transaction")
    println("  - Statement: eksekutor SQL (gunakan PreparedStatement untuk parameter)")
    println("  - ResultSet: iterator baris hasil query")

    println("\n=== JDBC 4.0+ auto-loading ===")
    println("Sejak JDBC 4.0, Class.forName() tidak wajib bila driver")
    println("sudah didaftarkan via ServiceLoader (META-INF/services/java.sql.Driver).")

    keeper.close()
}

Output yang Dihasilkan:

text
=== Connection ===
Auto-commit default: true

=== Statement.executeUpdate() (DDL) ===
Tabel 'produk' dibuat

=== Statement.executeUpdate() (INSERT) ===
Rows inserted: 3

=== Statement.executeQuery() (SELECT) ===
id | nama            | harga
---+-----------------+------
 1 | Kopi Susu       | 18000
 2 | Es Teh Manis    | 8000
 3 | Nasi Goreng     | 25000

=== Statement.execute() ===
Has ResultSet: true
Total baris: 3

=== Hierarki JDBC API ===
DriverManager -> Connection -> Statement -> ResultSet
  - DriverManager: factory untuk Connection berdasarkan URL
  - Connection: sesi ke database, mengatur transaction
  - Statement: eksekutor SQL (gunakan PreparedStatement untuk parameter)
  - ResultSet: iterator baris hasil query

=== JDBC 4.0+ auto-loading ===
Sejak JDBC 4.0, Class.forName() tidak wajib bila driver
sudah didaftarkan via ServiceLoader (META-INF/services/java.sql.Driver).

Kotlin Wrapper Extensions #

JDBC berbasis Java sehingga pemanggilan method bertele. Kita bisa membungkus pola umum menjadi extension function Kotlin agar lebih ringkas. Contoh wrapper membaca ResultSet menjadi List:

kotlin Extension: ResultSet.mapToList()
fun <T> ResultSet.toList(mapper: (ResultSet) -> T): List<T> {
    val result = mutableListOf<T>()
    while (this.next()) result.add(mapper(this))
    return result
}

// Pemakaian:
stmt.executeQuery("SELECT id, nama, harga FROM produk").use { rs ->
    val produkList = rs.toList {
        Produk(it.getInt("id"), it.getString("nama"), it.getInt("harga"))
    }
    println(produkList)
}

Wrapper seperti ini menjadi cikal-bakalan mini-ORM yang akan kita bahas di chapter ORM Fundamentals. Tetap perhatikan bahwa resource ResultSet dan Statement wajib ditutup via use { }, baik di kode mentah maupun di dalam wrapper.

Praktik Umum JDBC #

Beberapa praktik yang dipakai di kode produksi JDBC patut diperhatikan sejak awal. Pertama, gunakan PreparedStatement untuk query dengan parameter — selain aman dari SQL injection, query plan di- cache di sisi database sehingga eksekusi berulang lebih cepat. Kedua, batasi result set dengan LIMIT (atau FETCH FIRST n ROWS ONLY di standar SQL) untuk query yang berpotensi mengembalikan banyak baris. Ketiga, hindari SELECT * dan tulis nama kolom eksplisit agar kode tidak terpengaruh saat migrasi menambah kolom baru di masa depan. Keempat, pakai connection pool di aplikasi server — jangan membuka koneksi baru per request karena lambat dan boros resource database. Terakhir, tutup seluruh resource via finally atau use { } di Kotlin karena JDBC tidak terkait dengan garbage collection Java — kebocoran koneksi tidak akan otomatis dibersihkan.

Referensi Lanjutan #

Selanjutnya #

Dengan empat interface inti JDBC di tangan, kita siap membahas detail koneksi database pada chapter berikutnya — Connecting to Database — yang akan membedakan SQLite (embedded) dan PostgreSQL (client-server) serta pola lifecycle koneksi.