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).

java ShimJdbc.java
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:

text
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:xxx

Perhatikan urutan jejak [shim] pada output — urutan itulah alur JDBC yang sesungguhnya. Mari kita telaah bagian pentingnya:

  1. DriverManager.registerDriver(new DemoDriver()): class DemoDriver mengimplementasikan seluruh method java.sql.Driver (hanya tujuh method, termasuk getParentLogger). Mulai langkah ini, driver kita setara dengan driver vendor di mata DriverManager.
  2. DriverManager.getDriver("jdbc:demo:toko"): DriverManager memanggil acceptsURL pada setiap driver terdaftar dan mengembalikan driver pertama yang menerima URL tersebut — bukti bahwa pemilihan driver murni berdasarkan URL.
  3. connect(url, info) pada DemoDriver: driver mengembalikan null untuk URL yang bukan miliknya, sehingga DriverManager mencoba driver berikutnya. Untuk jdbc:demo:toko, driver mengembalikan objek Connection hasil ShimFactory.connection().
  4. ShimFactory.connection() dan kawan-kawan: Proxy.newProxyInstance membuat objek yang mengimplementasikan Connection; setiap pemanggilan method diteruskan ke lambda handler di parameter ketiga. Handler mencetak jejak panggilan lalu mengembalikan objek shim berikutnya — createStatement() menghasilkan Statement shim, executeQuery(sql) menghasilkan ResultSet shim.
  5. int[] kursor = {-1} dan case "next": ResultSet shim menyimpan kursor mulai dari -1 (sebelum baris pertama) — meniru kontrak ResultSet.next() yang akan kita bahas pada Chapter ResultSet.
  6. Urutan close() pada output: try-with-resources menutup resource dalam urutan kebalikan dari deklarasi — ResultSet, lalu Statement, lalu Connection. Urutan ini logis karena ResultSet bergantung pada Statement, dan Statement bergantung pada Connection.

Empat Tipe Driver JDBC #

Sejarah JDBC mengenal empat tipe driver dengan jalur komunikasi yang berbeda:

TipeNamaJalur ke databaseCatatan
1JDBC-ODBC bridgeJDBC → ODBC → databasesudah dihapus sejak Java 8; jangan dipakai
2native-APIJDBC → pustaka C vendorbutuh pustaka terpasang di mesin; jarang dipakai
3net-protocolJDBC → middleware server → databaseperlu server perantara; khusus kasus tertentu
4thin / native-protocolJDBC → protokol vendor via TCPpaling 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ShimJdbc.java

Unduh berkas
java
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;
    }
}