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:
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 MySQLURL 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.
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.
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.
// 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:
=== 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:
DriverManager.getConnection(url).use { conn ->
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT ...").use { rs ->
while (rs.next()) { /* ... */ }
}
}
}
// Semua resource di-close otomatis saat keluar blokPola 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 #
- java.sql package (Java SE 21)
— API reference
DriverManagerdanConnectiondi paketjava.sql
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.