Membuka koneksi ke database adalah langkah pertama yang terlihat sepele, tetapi pilihan URL dan cara mengelola koneksi sangat memengaruhi keandalan aplikasi. Koneksi yang tidak ditutup akan bocor; URL yang salah format akan gagal diam-diam; dan pilihan antara database embedded seperti SQLite dan server seperti PostgreSQL menentukan arsitektur deployment. Pada chapter ini kita akan membedah format JDBC URL, membandingkan SQLite dengan PostgreSQL, dan menulis kode yang membuka serta menutup koneksi dengan benar.

Format JDBC URL #

URL JDBC berbentuk jdbc:<subprotocol>:<subname>. Bagian <subprotocol> mengidentifikasi driver (sqlite, postgresql, mysql), sementara <subname> diisi sesuai konvensi masing-masing driver. Beberapa contoh:

text
jdbc:sqlite:file.db                                -> file lokal
jdbc:sqlite::memory:                               -> in-memory
jdbc:postgresql://db.example.com:5432/pesanan      -> server PostgreSQL
jdbc:mysql://db.example.com:3306/pesanan           -> server MySQL

URL dapat diikuti parameter query: ?user=appuser&password=PASS&sslmode=require. Parameter penting seperti sslmode (PostgreSQL) atau useSSL (MySQL) mengatur keamanan koneksi.

SQLite: Embedded, Tanpa Server #

SQLite adalah embedded database — library terkompilasi ke dalam aplikasi, bukan proses server terpisah. Data disimpan di satu file di disk atau di memori (:memory:). Konkurensi terbatas: biasanya satu writer pada satu waktu, beberapa reader paralel. SQLite cocok untuk aplikasi desktop, mobile, prototipe, dan pengujian unit.

kotlin URL SQLite
Class.forName("org.sqlite.JDBC")
val url = "jdbc:sqlite:file::memory:?cache=shared"
val conn = DriverManager.getConnection(url)

Karena tidak ada server, tidak ada host atau port. Kredensial biasanya tidak diperlukan (SQLite tidak mengelola user/password secara default).

PostgreSQL: Client-Server #

PostgreSQL adalah server database yang berjalan sebagai daemon di host tertentu (default port 5432). Aplikasi terhubung via TCP ke server, mengirim kredensial, lalu mendapat session. PostgreSQL mendukung MVCC (Multi-Version Concurrency Control) yang mengizinkan banyak writer paralel. Cocok untuk aplikasi web, microservice, dan skala produksi.

kotlin URL PostgreSQL
val url = "jdbc:postgresql://db.example.com:5432/pesanan" +
          "?user=appuser&password=PASS&sslmode=verify-full"

Kode Lengkap: Membuka Koneksi dan Inspeksi Metadata #

Kode berikut membuka koneksi SQLite in-memory, menyisipkan satu baris, lalu membacanya kembali. Kode juga menampilkan metadata koneksi (nama database, versi driver, URL) yang berguna untuk verifikasi. Sebagai pembanding, kode mem-parsing URL PostgreSQL contoh dan menampilkan komponennya.

kotlin
// connecting_to_database.kt
// Mengenal connection URL dan perbedaan SQLite (embedded) vs PostgreSQL
// (server). Memakai SQLite via JDBC agar bisa dijalankan tanpa server.

import java.sql.Connection
import java.sql.DriverManager

fun main() {
    Class.forName("org.sqlite.JDBC")

    println("=== Format JDBC URL ===")
    println("jdbc:<subprotocol>:<subname>")
    println("  SQLite  : jdbc:sqlite:file.db         (file-based)")
    println("  SQLite  : jdbc:sqlite::memory:         (in-memory)")
    println("  SQLite  : jdbc:sqlite:file::memory:?cache=shared  (in-memory shared)")
    println("  Postgres: jdbc:postgresql://host:5432/db?user=USER&password=PASS")
    println("  MySQL   : jdbc:mysql://host:3306/db?useSSL=true")

    // SQLite: embedded, no server
    println("\n=== SQLite (embedded, no server) ===")
    val sqliteUrl = "jdbc:sqlite:file::memory:?cache=shared"
    val keeper: Connection = DriverManager.getConnection(sqliteUrl)
    DriverManager.getConnection(sqliteUrl).use { conn ->
        conn.metaData.let { md ->
            println("Database: ${md.databaseProductName} ${md.databaseProductVersion}")
            println("Driver  : ${md.driverName} ${md.driverVersion}")
            println("URL     : ${md.url}")
            println("User    : ${md.userName ?: "(default)"}")
        }
        conn.createStatement().use { stmt ->
            stmt.execute("CREATE TABLE IF NOT EXISTS log (msg TEXT)")
            stmt.executeUpdate("INSERT INTO log VALUES ('Hello dari SQLite in-memory')")
        }
        conn.createStatement().use { stmt ->
            stmt.executeQuery("SELECT msg FROM log").use { rs ->
                if (rs.next()) println("Read back: ${rs.getString("msg")}")
            }
        }
    }
    println("Ciri SQLite: tidak ada server, file-based atau :memory:, satu proses menulis")

    // Simulasi PostgreSQL URL (tanpa benar-benar connect)
    println("\n=== PostgreSQL URL (decoded, simulasi) ===")
    val pgUrl = "jdbc:postgresql://db.example.com:5432/pesanan?user=appuser&password=PASS&sslmode=require"
    println("URL: $pgUrl")
    val parsed = parseJdbcUrl(pgUrl)
    println("  subprotocol: ${parsed["subprotocol"]}")
    println("  host       : ${parsed["host"]}")
    println("  port       : ${parsed["port"]}")
    println("  database   : ${parsed["database"]}")
    println("  user       : ${parsed["user"]}")
    println("  sslmode    : ${parsed["sslmode"]}")
    println("Ciri Postgres: server terpisah, butuh host:port, butuh kredensial, dukung TLS")

    println("\n=== Perbandingan ===")
    println("| Aspek           | SQLite            | PostgreSQL              |")
    println("|-----------------|-------------------|-------------------------|")
    println("| Arsitektur      | Embedded library  | Client-server           |")
    println("| Server          | Tidak ada         | Wajib (postgres daemon) |")
    println("| Konkurensi      | 1 writer, N reader| Multi-writer (MVCC)     |")
    println("| URL memerlukan  | Path file         | host:port, db, user     |")
    println("| Cocok untuk     | App desktop, test | App web, skala produksi |")

    println("\n=== Catatan koneksi ===")
    println("Connection wajib di-close via .use{} atau try-with-resources")
    println("  agar resource DB tidak bocor dan kembali ke pool.")
    println("Setelah close, Connection tidak bisa dipakai lagi.")
    keeper.close()
}

// Parser URL JDBC postgres-like sederhana
fun parseJdbcUrl(url: String): Map<String, String> {
    val result = mutableMapOf<String, String>()
    // jdbc:postgresql://host:port/db?params
    val afterProto = url.removePrefix("jdbc:")
    val (subproto, rest) = afterProto.split(":", limit = 2)
    result["subprotocol"] = subproto
    // postgresql://host:port/db?params
    val afterSlashes = rest.removePrefix("//")
    val (authorityDb, params) = afterSlashes.split("?", limit = 2)
    val (authority, db) = authorityDb.split("/", limit = 2)
    val (host, port) = authority.split(":", limit = 2)
    result["host"] = host
    result["port"] = port
    result["database"] = db
    params.split("&").forEach { p ->
        val (k, v) = p.split("=", limit = 2)
        result[k] = v
    }
    return result
}

Output yang Dihasilkan:

text
=== Format JDBC URL ===
jdbc:<subprotocol>:<subname>
  SQLite  : jdbc:sqlite:file.db         (file-based)
  SQLite  : jdbc:sqlite::memory:         (in-memory)
  SQLite  : jdbc:sqlite:file::memory:?cache=shared  (in-memory shared)
  Postgres: jdbc:postgresql://host:5432/db?user=USER&password=PASS
  MySQL   : jdbc:mysql://host:3306/db?useSSL=true

=== SQLite (embedded, no server) ===
Database: SQLite 3.46.1
Driver  : SQLite JDBC 3.46.1.3
URL     : jdbc:sqlite:file::memory:?cache=shared
User    : (default)
Read back: Hello dari SQLite in-memory
Ciri SQLite: tidak ada server, file-based atau :memory:, satu proses menulis

=== PostgreSQL URL (decoded, simulasi) ===
URL: jdbc:postgresql://db.example.com:5432/pesanan?user=appuser&password=PASS&sslmode=require
  subprotocol: postgresql
  host       : db.example.com
  port       : 5432
  database   : pesanan
  user       : appuser
  sslmode    : require
Ciri Postgres: server terpisah, butuh host:port, butuh kredensial, dukung TLS

=== Perbandingan ===
| Aspek           | SQLite            | PostgreSQL              |
|-----------------|-------------------|-------------------------|
| Arsitektur      | Embedded library  | Client-server           |
| Server          | Tidak ada         | Wajib (postgres daemon) |
| Konkurensi      | 1 writer, N reader| Multi-writer (MVCC)     |
| URL memerlukan  | Path file         | host:port, db, user     |
| Cocok untuk     | App desktop, test | App web, skala produksi |

=== Catatan koneksi ===
Connection wajib di-close via .use{} atau try-with-resources
  agar resource DB tidak bocor dan kembali ke pool.
Setelah close, Connection tidak bisa dipakai lagi.

Lifecycle Koneksi #

Setiap Connection yang dibuka harus ditutup. Di Kotlin, pola use { } menangani ini otomatis — close() dipanggil di akhir blok, bahkan saat ada exception:

kotlin Pola use [ ]
DriverManager.getConnection(url).use { conn ->
    conn.createStatement().use { stmt ->
        stmt.executeQuery("SELECT ...").use { rs ->
            while (rs.next()) { /* ... */ }
        }
    }
}
// Semua resource di-close otomatis saat keluar blok

Pola nested use ini menutup resource dari dalam ke luar: ResultSet → Statement → Connection. Untuk aplikasi server yang banyak membuka koneksi, pola ini masih terlalu mahal karena setiap pemanggilan membuka koneksi TCP baru. Solusinya adalah connection pool yang akan kita bahas di chapter Connection Pooling.

Lifecycle koneksi yang baik juga melibatkan manajemen transaksi yang benar. Bila kita mematikan auto-commit untuk transaksi manual, kita wajib memastikan koneksi dikembalikan ke pool dalam keadaan bersih — yaitu auto-commit kembali ke true dan tidak ada transaksi yang menggantung. Tanpa ini, koneksi berikutnya yang dipinjam dari pool bisa mewarisi state transaksi yang salah dan menyebabkan bug yang sulit dilacak.

Referensi Lanjutan #

Selanjutnya #

Setelah memahami cara membuka koneksi, kita siap mengeksekusi SQL melalui koneksi tersebut. Chapter berikutnya — Executing SQL Queries — akan membedakan execute(), executeQuery(), dan executeUpdate() serta kapan memakai masing-masing.