Dunia database sangat beragam: SQLite, PostgreSQL, MySQL, Oracle, SQL Server — masing-masing punya protokol jaringan dan cara paket data sendiri. Kalau tiap database menuntut kode yang berbeda, aplikasi Java akan terbelenggu ke satu produk. JDBC (Java Database Connectivity) memecahkan masalah itu dengan satu pustaka standar: aplikasi menulis kode seragam memakai interface di package java.sql, dan perbedaan antar database diserap oleh lapisan driver. Chapter ini membedah arsitektur lapisan demi lapisan, lalu membuktikan alurnya lewat driver kecil buatan kita sendiri.
Lapisan Arsitektur JDBC #
Ketika sebuah baris SELECT berjalan, kode kita melewati empat lapisan bertingkat:
Lapisan paling atas adalah kode aplikasi — class-class yang kita tulis, yang hanya mengenal Connection, Statement, dan ResultSet. Lapisan kedua adalah JDBC API di module java.sql: kumpulan interface dan class standar milik JDK, sehingga tidak ada satu baris pun kode vendor di aplikasi kita. Lapisan ketiga, DriverManager, adalah petugas perutean: diberi sebuah URL JDBC, DriverManager memilih driver yang cocok dari daftar driver terdaftar. Lapisan keempat, driver, adalah pustaka .jar terpisah dari vendor (misalnya sqlite-jdbc atau driver PostgreSQL) yang menerjemahkan pemanggilan interface standar menjadi protokol khusus database tersebut — melalui jaringan atau pembacaan berkas.
DriverManager dan Pemilihan Driver #
DriverManager menyimpan daftar driver yang terdaftar dan bekerja seperti pola registry. Saat DriverManager.getConnection(url) dipanggil, DriverManager menelusuri daftar tersebut satu per satu dan memanggil driver.connect(url, info). Driver yang merasa URL bukan miliknya cukup mengembalikan null sebagai tanda menyerah, sehingga DriverManager melanjutkan ke driver berikutnya; driver yang mengaku memiliki URL mengembalikan objek Connection, dan objek itulah yang dikirim balik ke aplikasi. Bila tidak satu driver pun cocok, DriverManager melempar SQLException dengan pesan No suitable driver — kode galat yang akan Anda temui cepat atau lambat saat URL salah ketik atau driver lupa diletakkan di classpath.
Membuktikan Alur tanpa Database Nyata #
Lingkungan uji Course ini tidak memasang JDBC driver manapun — tidak ada sqlite-jdbc, tidak ada server database. Justru situasi ini memberi kita keuntungan bedah: kita bisa membuat driver sendiri yang mengimplementasikan java.sql.Driver, mendaftarkannya ke DriverManager, lalu mengamati seluruh alur pemanggilan tanpa database sungguhan. Objek Connection, Statement, dan ResultSet dibuat memakai Proxy — teknik refleksi yang membuat objek “seolah-olah” mengimplementasikan interface tanpa menulis puluhan method-nya (teknik ini dibahas tuntas pada Section Annotations dan Reflection).
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
public class ShimJdbc {
public static void main(String[] args) throws SQLException {
// Langkah 1: driver buatan sendiri didaftarkan ke DriverManager
DriverManager.registerDriver(new DemoDriver());
// Langkah 2: DriverManager memilih driver berdasarkan URL JDBC
Driver dipilih = DriverManager.getDriver("jdbc:demo:toko");
System.out.println("driver terpilih : " + dipilih.getClass().getSimpleName());
System.out.println("versi driver : " + dipilih.getMajorVersion()
+ "." + dipilih.getMinorVersion());
// Langkah 3: alur lengkap getConnection -> createStatement -> executeQuery
try (Connection conn = DriverManager.getConnection("jdbc:demo:toko");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT nama, kota FROM pelanggan")) {
while (rs.next()) {
System.out.println(rs.getString("nama") + " | " + rs.getString("kota"));
}
}
// Langkah 4: URL yang tidak dikenal driver mana pun ditolak
try {
DriverManager.getConnection("jdbc:lain:xxx");
} catch (SQLException e) {
System.out.println("ditolak: " + e.getMessage());
}
}
}
class DemoDriver implements Driver {
@Override
public Connection connect(String url, Properties info) {
if (url == null || !url.startsWith("jdbc:demo:")) {
return null; // bukan milik driver ini: DriverManager mencoba driver lain
}
System.out.println("[shim] connect(" + url + ") -> objek Connection demo dibuat");
return ShimFactory.connection();
}
@Override
public boolean acceptsURL(String url) {
return url != null && url.startsWith("jdbc:demo:");
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
return new DriverPropertyInfo[0];
}
@Override
public int getMajorVersion() {
return 1;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public java.util.logging.Logger getParentLogger() {
return null;
}
}
class ShimFactory {
static final List<String[]> BARIS = List.of(
new String[]{"Budi", "Jakarta"},
new String[]{"Siti", "Bandung"},
new String[]{"Dewi", "Surabaya"});
static final List<String> KOLOM = List.of("nama", "kota");
static Connection connection() {
return (Connection) Proxy.newProxyInstance(
Connection.class.getClassLoader(),
new Class<?>[]{Connection.class},
(proxy, method, args) -> {
if (method.getName().equals("createStatement")) {
System.out.println("[shim] Connection.createStatement() dipanggil");
return statement();
}
return umum("Connection", proxy, method);
});
}
static Statement statement() {
return (Statement) Proxy.newProxyInstance(
Statement.class.getClassLoader(),
new Class<?>[]{Statement.class},
(proxy, method, args) -> {
if (method.getName().equals("executeQuery")) {
System.out.println("[shim] Statement.executeQuery(\""
+ args[0] + "\") dipanggil");
return resultSet();
}
return umum("Statement", proxy, method);
});
}
static ResultSet resultSet() {
int[] kursor = {-1}; // kursor mulai SEBELUM baris pertama
return (ResultSet) Proxy.newProxyInstance(
ResultSet.class.getClassLoader(),
new Class<?>[]{ResultSet.class},
(proxy, method, args) -> {
switch (method.getName()) {
case "next" -> {
kursor[0]++;
return kursor[0] < BARIS.size();
}
case "getString" -> {
int idx = (args[0] instanceof Integer angka)
? angka - 1
: KOLOM.indexOf(args[0]);
return BARIS.get(kursor[0])[idx];
}
case "close" -> {
System.out.println("[shim] ResultSet.close()");
return null;
}
default -> {
return umum("ResultSet", proxy, method);
}
}
});
}
/** Penanganan method umum: close, toString, dan method yang tidak kita pakai. */
static Object umum(String label, Object proxy, Method method) {
if (method.getName().equals("close")) {
System.out.println("[shim] " + label + ".close()");
return null;
}
if (method.getName().equals("toString")) {
return "Shim" + label;
}
Class<?> t = method.getReturnType();
if (t == boolean.class) {
return false;
}
if (t == int.class) {
return 0;
}
if (t == long.class) {
return 0L;
}
return null;
}
}Output yang Dihasilkan:
driver terpilih : DemoDriver
versi driver : 1.0
[shim] connect(jdbc:demo:toko) -> objek Connection demo dibuat
[shim] Connection.createStatement() dipanggil
[shim] Statement.executeQuery("SELECT nama, kota FROM pelanggan") dipanggil
Budi | Jakarta
Siti | Bandung
Dewi | Surabaya
[shim] ResultSet.close()
[shim] Statement.close()
[shim] Connection.close()
ditolak: No suitable driver found for jdbc:lain:xxxPerhatikan urutan jejak [shim] pada output — urutan itulah alur JDBC yang sesungguhnya. Mari kita telaah bagian pentingnya:
DriverManager.registerDriver(new DemoDriver()): classDemoDrivermengimplementasikan seluruh methodjava.sql.Driver(hanya tujuh method, termasukgetParentLogger). Mulai langkah ini, driver kita setara dengan driver vendor di mata DriverManager.DriverManager.getDriver("jdbc:demo:toko"): DriverManager memanggilacceptsURLpada setiap driver terdaftar dan mengembalikan driver pertama yang menerima URL tersebut — bukti bahwa pemilihan driver murni berdasarkan URL.connect(url, info)padaDemoDriver: driver mengembalikannulluntuk URL yang bukan miliknya, sehingga DriverManager mencoba driver berikutnya. Untukjdbc:demo:toko, driver mengembalikan objekConnectionhasilShimFactory.connection().ShimFactory.connection()dan kawan-kawan:Proxy.newProxyInstancemembuat objek yang mengimplementasikanConnection; setiap pemanggilan method diteruskan ke lambda handler di parameter ketiga. Handler mencetak jejak panggilan lalu mengembalikan objek shim berikutnya —createStatement()menghasilkanStatementshim,executeQuery(sql)menghasilkanResultSetshim.int[] kursor = {-1}dancase "next":ResultSetshim menyimpan kursor mulai dari-1(sebelum baris pertama) — meniru kontrakResultSet.next()yang akan kita bahas pada Chapter ResultSet.- Urutan
close()pada output:try-with-resources menutup resource dalam urutan kebalikan dari deklarasi —ResultSet, laluStatement, laluConnection. Urutan ini logis karenaResultSetbergantung padaStatement, danStatementbergantung padaConnection.
Empat Tipe Driver JDBC #
Sejarah JDBC mengenal empat tipe driver dengan jalur komunikasi yang berbeda:
| Tipe | Nama | Jalur ke database | Catatan |
|---|---|---|---|
| 1 | JDBC-ODBC bridge | JDBC → ODBC → database | sudah dihapus sejak Java 8; jangan dipakai |
| 2 | native-API | JDBC → pustaka C vendor | butuh pustaka terpasang di mesin; jarang dipakai |
| 3 | net-protocol | JDBC → middleware server → database | perlu server perantara; khusus kasus tertentu |
| 4 | thin / native-protocol | JDBC → protokol vendor via TCP | paling umum; murni Java, tanpa pustaka C |
Driver modern yang akan kita pakai sepanjang Course — sqlite-jdbc, PostgreSQL, MySQL Connector/J — semuanya bertipe 4. Driver tipe 4 berupa satu berkas .jar yang cukup diletakkan di classpath, tanpa instalasi pustaka tambahan di sistem operasi.
java.sql dan javax.sql #
API JDBC dibagi ke dua package. Package java.sql memuat inti pemrograman sehari-hari: DriverManager, Connection, Statement, PreparedStatement, ResultSet, dan SQLException. Package javax.sql (satu module yang sama, java.sql) memuat API tingkat lanjut untuk kontainer dan pooling: DataSource sebagai pengganti DriverManager pada aplikasi modern, ConnectionPoolDataSource, serta RowSet. Untuk sebagian besar kode pada section ini kita bermain di java.sql; DataSource akan menjadi pusat pembahasan pada Chapter Connection Pooling.
Dengan arsitektur yang kini terbaca utuh — aplikasi, API standar, DriverManager sebagai perute, dan driver sebagai penerjemah — langkah berikutnya adalah memahami keping yang paling sering membuat orang tersandung: bagaimana driver ditemukan, didaftarkan, dan diberi alamat lewat URL JDBC.
Referensi #
- Oracle: package java.sql (JDK 25)
- Oracle: DriverManager (JDK 25)
- Oracle: interface Driver (JDK 25)
- dev.java — Learn Java
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ShimJdbc.java
Unduh berkasimport java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
public class ShimJdbc {
public static void main(String[] args) throws SQLException {
// Langkah 1: driver buatan sendiri didaftarkan ke DriverManager
DriverManager.registerDriver(new DemoDriver());
// Langkah 2: DriverManager memilih driver berdasarkan URL JDBC
Driver dipilih = DriverManager.getDriver("jdbc:demo:toko");
System.out.println("driver terpilih : " + dipilih.getClass().getSimpleName());
System.out.println("versi driver : " + dipilih.getMajorVersion()
+ "." + dipilih.getMinorVersion());
// Langkah 3: alur lengkap getConnection -> createStatement -> executeQuery
try (Connection conn = DriverManager.getConnection("jdbc:demo:toko");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT nama, kota FROM pelanggan")) {
while (rs.next()) {
System.out.println(rs.getString("nama") + " | " + rs.getString("kota"));
}
}
// Langkah 4: URL yang tidak dikenal driver mana pun ditolak
try {
DriverManager.getConnection("jdbc:lain:xxx");
} catch (SQLException e) {
System.out.println("ditolak: " + e.getMessage());
}
}
}
class DemoDriver implements Driver {
@Override
public Connection connect(String url, Properties info) {
if (url == null || !url.startsWith("jdbc:demo:")) {
return null; // bukan milik driver ini: DriverManager mencoba driver lain
}
System.out.println("[shim] connect(" + url + ") -> objek Connection demo dibuat");
return ShimFactory.connection();
}
@Override
public boolean acceptsURL(String url) {
return url != null && url.startsWith("jdbc:demo:");
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
return new DriverPropertyInfo[0];
}
@Override
public int getMajorVersion() {
return 1;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public java.util.logging.Logger getParentLogger() {
return null;
}
}
class ShimFactory {
static final List<String[]> BARIS = List.of(
new String[]{"Budi", "Jakarta"},
new String[]{"Siti", "Bandung"},
new String[]{"Dewi", "Surabaya"});
static final List<String> KOLOM = List.of("nama", "kota");
static Connection connection() {
return (Connection) Proxy.newProxyInstance(
Connection.class.getClassLoader(),
new Class<?>[]{Connection.class},
(proxy, method, args) -> {
if (method.getName().equals("createStatement")) {
System.out.println("[shim] Connection.createStatement() dipanggil");
return statement();
}
return umum("Connection", proxy, method);
});
}
static Statement statement() {
return (Statement) Proxy.newProxyInstance(
Statement.class.getClassLoader(),
new Class<?>[]{Statement.class},
(proxy, method, args) -> {
if (method.getName().equals("executeQuery")) {
System.out.println("[shim] Statement.executeQuery(\""
+ args[0] + "\") dipanggil");
return resultSet();
}
return umum("Statement", proxy, method);
});
}
static ResultSet resultSet() {
int[] kursor = {-1}; // kursor mulai SEBELUM baris pertama
return (ResultSet) Proxy.newProxyInstance(
ResultSet.class.getClassLoader(),
new Class<?>[]{ResultSet.class},
(proxy, method, args) -> {
switch (method.getName()) {
case "next" -> {
kursor[0]++;
return kursor[0] < BARIS.size();
}
case "getString" -> {
int idx = (args[0] instanceof Integer angka)
? angka - 1
: KOLOM.indexOf(args[0]);
return BARIS.get(kursor[0])[idx];
}
case "close" -> {
System.out.println("[shim] ResultSet.close()");
return null;
}
default -> {
return umum("ResultSet", proxy, method);
}
}
});
}
/** Penanganan method umum: close, toString, dan method yang tidak kita pakai. */
static Object umum(String label, Object proxy, Method method) {
if (method.getName().equals("close")) {
System.out.println("[shim] " + label + ".close()");
return null;
}
if (method.getName().equals("toString")) {
return "Shim" + label;
}
Class<?> t = method.getReturnType();
if (t == boolean.class) {
return false;
}
if (t == int.class) {
return 0;
}
if (t == long.class) {
return 0L;
}
return null;
}
}