executeQuery membawa pulang sebuah ResultSet — dan di sinilah hasil kerja database berubah menjadi data Java. ResultSet bukan List berisi baris; ia adalah kursor yang berdiri di antara baris-baris hasil dan bergerak maju satu per satu lewat next(). Dari posisi kursor itulah kita membaca kolom memakai getter bertipe, dengan dua alamat pilihan: indeks angka atau label nama kolom. Ada satu jebakan klasik yang menunggu di ujung chapter — nilai NULL yang dibaca getInt() tampak seperti angka nol biasa — dan kita akan mengatasinya dengan wasNull().

Kursor dan Method next() #

Kontrak ResultSet menempatkan kursor sebelum baris pertama saat objek dibuat. Karena itu pola baca JDBC selalu diawali pemanggilan next(): method ini menggeser kursor satu baris maju, mengembalikan true jika kursor mendarat pada baris data, dan false jika hasil sudah habis. Perhatikan posisi kursor pada ilustrasi berikut:

text
posisi awal  ->  [baris 1]  ->  [baris 2]  ->  [baris 3]  ->  habis
   (-1)          kursor=0       kursor=1       kursor=2      next() = false

Membaca kolom sebelum next() pertama adalah kesalahan pemula nomor satu: kursor belum menunjuk baris mana pun, sehingga getter melempar exception. Membaca setelah next() terakhir mengembalikan false sama-sama salah, dengan alasan kebalikannya. Iterasi idiomatik JDBC karenanya berbentuk while (rs.next()) { ... } — satu method sekaligus menjadi penjaga perulangan dan penanda akhir data.

Getter Bertipe dan Pemetaan Tipe #

Getter ResultSet dipilih menurut tipe kolom, dan tiap tipe SQL punya pasangan Java yang lazim:

Tipe SQLGetterTipe Java kembalian
INTEGERgetInt(i) / getLong(i)int / long
VARCHAR, TEXTgetString(i)String
DOUBLE, FLOATgetDouble(i)double
DECIMALgetBigDecimal(i)BigDecimal
DATEgetDate(i)java.sql.Date
TIMESTAMPgetTimestamp(i)java.sql.Timestamp
BOOLEANgetBoolean(i)boolean
tipe apa pungetObject(i)Object (driver memilih tipe terdekat)

Mekanisme pembacaannya lugas: driver mengambil bytes dari protokol database, mengonversi ke tipe Java yang diminta, lalu mengembalikannya. Konversi yang tidak wajar — misalnya getString pada kolom numerik — umumnya masih diizinkan (angka diubah menjadi teks), tetapi membaca teks "Jakarta" dengan getInt akan melempar exception konversi.

Indeks Berbasis 1 dan Label Kolom #

Kolom dialamatkan dengan indeks berbasis 1 — kolom pertama hasil SELECT adalah indeks 1, bukan 0 seperti array Java. Aturan ini diwariskan dari dunia SQL yang menghitung kolom mulai satu, dan menjadi sumber bug klasik berupa ArrayIndexOutOfBounds-nya JDBC. Alternatif yang lebih tahan perubahan adalah label: getString("nama") membaca kolom hasil yang bernama nama pada klausa SELECT. Label unggul saat urutan kolom berpotensi berubah karena query dirombak; indeks unggul pada kueri berhitungan kolom pendek seperti SELECT COUNT(*).

NULL dan wasNull #

Masalah NULL: kolom boleh kosong, tetapi int Java tidak bisa “kosong”. Kontrak JDBC menyiasatinya dengan dua langkah — getter primitif mengembalikan nilai nol (0, 0.0, false) untuk kolom NULL, dan method wasNull() melaporkan apakah pembacaan terakhir memang NULL. wasNull() wajib dipanggil segera setelah getter yang ingin diperiksa, karena setiap pembacaan baru menimpa statusnya. Tanpa pola ini, stok yang tidak diketahui (NULL) dan stok yang memang nol jadi tak terbedakan.

Menggulir Hasil dalam Praktik #

Program berikut memakai class HasilKueri yang meniru kontrak java.sql.ResultSet di atas data in-memory — nama method sengaja dibuat sama persis sehingga setiap perilaku yang kita buktikan berlaku juga pada ResultSet asli. Stok produk kedua sengaja dibiarkan NULL:

java BacaHasil.java
import java.util.List;

public class BacaHasil {

    public static void main(String[] args) {
        HasilKueri hasil = DemoDb.query("SELECT id, nama, harga, stok FROM produk");

        System.out.println("jumlah kolom : " + hasil.getJumlahKolom());
        if (hasil.next()) {
            System.out.println("label vs indeks  : \"" + hasil.getString("nama")
                    + "\" == \"" + hasil.getString(2) + "\"");
            hasil.beforeFirst();
            System.out.println("kursor dimundurkan lewat beforeFirst()");
        }

        System.out.println();
        while (hasil.next()) {
            int id = hasil.getInt(1);
            String nama = hasil.getString("nama");
            double harga = hasil.getDouble(3);
            int stok = hasil.getInt("stok");
            String stokTampil = hasil.wasNull() ? "?" : String.valueOf(stok);
            System.out.printf("%d  %-9s Rp%.2f  stok=%s%n", id, nama, harga, stokTampil);
        }

        System.out.println();
        System.out.println("next() setelah baris terakhir -> " + hasil.next());

        System.out.println();
        System.out.println("== Jebakan umum ==");
        HasilKueri ringkas = DemoDb.query("SELECT id, nama FROM produk");
        try {
            ringkas.getInt(1); // kursor masih sebelum baris pertama
        } catch (IllegalStateException e) {
            System.out.println("baca sebelum next() : " + e.getMessage());
        }
        ringkas.next();
        try {
            ringkas.getInt(4); // hanya ada 2 kolom
        } catch (IllegalArgumentException e) {
            System.out.println("indeks di luar kolom: " + e.getMessage());
        }
    }
}

/** Meniru kontrak java.sql.ResultSet dengan data in-memory. */
class HasilKueri {

    private final List<String> kolom;
    private final List<Object[]> baris;
    private int kursor = -1;              // -1 = sebelum baris pertama
    private boolean nullTerakhir = false;

    HasilKueri(List<String> kolom, List<Object[]> baris) {
        this.kolom = List.copyOf(kolom);
        this.baris = List.copyOf(baris);
    }

    int getJumlahKolom() {
        return kolom.size();
    }

    void beforeFirst() {
        kursor = -1;
    }

    boolean next() {
        if (kursor < baris.size()) {
            kursor++;
        }
        return kursor < baris.size();
    }

    boolean wasNull() {
        return nullTerakhir;
    }

    String getString(String label) {
        return (String) nilai(indeksKolom(label));
    }

    String getString(int indeks) {
        return (String) nilai(indeksKolom(indeks));
    }

    int getInt(String label) {
        return toInt(nilai(indeksKolom(label)));
    }

    int getInt(int indeks) {
        return toInt(nilai(indeksKolom(indeks)));
    }

    double getDouble(int indeks) {
        Object v = nilai(indeksKolom(indeks));
        return v == null ? 0.0 : (Double) v;
    }

    Object getObject(int indeks) {
        return nilai(indeksKolom(indeks));
    }

    private int toInt(Object v) {
        return v == null ? 0 : (Integer) v;
    }

    private Object nilai(int idxData) {
        if (kursor < 0 || kursor >= baris.size()) {
            throw new IllegalStateException(
                    "kursor tidak berada pada baris yang valid (panggil next() lebih dulu)");
        }
        Object v = baris.get(kursor)[idxData];
        nullTerakhir = (v == null);
        return v;
    }

    private int indeksKolom(int indeks) {
        if (indeks < 1 || indeks > kolom.size()) {
            throw new IllegalArgumentException(
                    "indeks kolom tidak valid: " + indeks
                            + " (jumlah kolom: " + kolom.size() + ")");
        }
        return indeks - 1; // kontrak JDBC: indeks berbasis 1
    }

    private int indeksKolom(String label) {
        int idx = kolom.indexOf(label);
        if (idx < 0) {
            throw new IllegalArgumentException("kolom tidak ditemukan: " + label);
        }
        return idx;
    }
}

/** "Database" demo dengan dua kueri yang dikenal. */
class DemoDb {

    static HasilKueri query(String sql) {
        List<Object[]> semua = List.of(
                new Object[]{1, "Buku Java", 150_000.00, 12},
                new Object[]{2, "Mouse", 250_000.00, null},
                new Object[]{3, "Keyboard", 450_000.00, 7});
        if (sql.equals("SELECT id, nama, harga, stok FROM produk")) {
            return new HasilKueri(List.of("id", "nama", "harga", "stok"), semua);
        }
        if (sql.equals("SELECT id, nama FROM produk")) {
            return new HasilKueri(List.of("id", "nama"), semua);
        }
        throw new IllegalArgumentException("kueri tidak dikenal: " + sql);
    }
}

Output yang Dihasilkan:

text
jumlah kolom : 4
label vs indeks  : "Buku Java" == "Buku Java"
kursor dimundurkan lewat beforeFirst()

1  Buku Java Rp150000.00  stok=12
2  Mouse     Rp250000.00  stok=?
3  Keyboard  Rp450000.00  stok=7

next() setelah baris terakhir -> false

== Jebakan umum ==
baca sebelum next() : kursor tidak berada pada baris yang valid (panggil next() lebih dulu)
indeks di luar kolom: indeks kolom tidak valid: 4 (jumlah kolom: 2)

Mari kita telaah perilaku yang terbukti pada output di atas:

  1. getString("nama") dan getString(2) menghasilkan nilai yang sama: label nama dan indeks 2 hanyalah dua alamat menuju kolom yang sama — bukti kedua gaya akses setara. Setelah itu beforeFirst() memundurkan kursor ke posisi awal sehingga perulangan berikut tetap membaca dari baris pertama.
  2. while (hasil.next()) menggulir tepat tiga baris: next() mengembalikan true tiga kali lalu false, dan perulangan berhenti tanpa perlu mengetahui jumlah baris lebih dulu.
  3. stok=? pada baris kedua: nilai stok Mouse memang NULL. Getter getInt mengembalikan 0, tetapi wasNull() yang dipanggil tepat setelahnya melaporkan true, sehingga program menampilkan ? alih-alih angka 0 yang menyesatkan.
  4. next() setelah baris terakhir mengembalikan false: kursor boleh dipanggil lagi tanpa exception; yang dilarang adalah membaca kolom saat kursor tidak menunjuk baris.
  5. Dua jebakan di akhir output: membaca sebelum next() pertama melempar exception karena kursor masih di posisi -1, dan getInt(4) pada kueri dua kolom ditolak karena indeks valid hanya 1 sampai 2 — padanan SQLException pada ResultSet sungguhan.

Pada database sungguhan, seluruh pola ini sama persis — hanya sumber datanya yang berubah. Cuplikan berikut menampilkan bentuk lengkapnya dengan SQLite (driver tidak terpasang di lingkungan uji kita, jadi tidak ada klaim output):

java Iterasi ResultSet dengan SQLite (butuh driver xerial)
String sql = "SELECT id, nama, harga, stok FROM produk";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:toko.db");
     PreparedStatement ps = conn.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {

    while (rs.next()) {
        int id = rs.getInt(1);
        String nama = rs.getString("nama");
        int stok = rs.getInt("stok");
        if (rs.wasNull()) {
            System.out.println(id + " " + nama + " stok tidak diketahui");
        } else {
            System.out.println(id + " " + nama + " stok " + stok);
        }
    }
}

Kita kini bisa membaca hasil kueri dengan aman. Namun satu INSERT debit dan satu UPDATE kredit pada tabel rekening masih berjalan sendiri-sendiri — jika program berhenti di antara keduanya, uang menguap tanpa jejak. Chapter berikutnya memperkenalkan transaksi JDBC: setAutoCommit(false), commit(), rollback(), dan jaminan ACID yang menyatukan langkah-langkah itu.

Referensi #

Berkas Sumber

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

BacaHasil.java

Unduh berkas
java
import java.util.List;

public class BacaHasil {

    public static void main(String[] args) {
        HasilKueri hasil = DemoDb.query("SELECT id, nama, harga, stok FROM produk");

        System.out.println("jumlah kolom : " + hasil.getJumlahKolom());
        if (hasil.next()) {
            System.out.println("label vs indeks  : \"" + hasil.getString("nama")
                    + "\" == \"" + hasil.getString(2) + "\"");
            hasil.beforeFirst();
            System.out.println("kursor dimundurkan lewat beforeFirst()");
        }

        System.out.println();
        while (hasil.next()) {
            int id = hasil.getInt(1);
            String nama = hasil.getString("nama");
            double harga = hasil.getDouble(3);
            int stok = hasil.getInt("stok");
            String stokTampil = hasil.wasNull() ? "?" : String.valueOf(stok);
            System.out.printf("%d  %-9s Rp%.2f  stok=%s%n", id, nama, harga, stokTampil);
        }

        System.out.println();
        System.out.println("next() setelah baris terakhir -> " + hasil.next());

        System.out.println();
        System.out.println("== Jebakan umum ==");
        HasilKueri ringkas = DemoDb.query("SELECT id, nama FROM produk");
        try {
            ringkas.getInt(1); // kursor masih sebelum baris pertama
        } catch (IllegalStateException e) {
            System.out.println("baca sebelum next() : " + e.getMessage());
        }
        ringkas.next();
        try {
            ringkas.getInt(4); // hanya ada 2 kolom
        } catch (IllegalArgumentException e) {
            System.out.println("indeks di luar kolom: " + e.getMessage());
        }
    }
}

/** Meniru kontrak java.sql.ResultSet dengan data in-memory. */
class HasilKueri {

    private final List<String> kolom;
    private final List<Object[]> baris;
    private int kursor = -1;              // -1 = sebelum baris pertama
    private boolean nullTerakhir = false;

    HasilKueri(List<String> kolom, List<Object[]> baris) {
        this.kolom = List.copyOf(kolom);
        this.baris = List.copyOf(baris);
    }

    int getJumlahKolom() {
        return kolom.size();
    }

    void beforeFirst() {
        kursor = -1;
    }

    boolean next() {
        if (kursor < baris.size()) {
            kursor++;
        }
        return kursor < baris.size();
    }

    boolean wasNull() {
        return nullTerakhir;
    }

    String getString(String label) {
        return (String) nilai(indeksKolom(label));
    }

    String getString(int indeks) {
        return (String) nilai(indeksKolom(indeks));
    }

    int getInt(String label) {
        return toInt(nilai(indeksKolom(label)));
    }

    int getInt(int indeks) {
        return toInt(nilai(indeksKolom(indeks)));
    }

    double getDouble(int indeks) {
        Object v = nilai(indeksKolom(indeks));
        return v == null ? 0.0 : (Double) v;
    }

    Object getObject(int indeks) {
        return nilai(indeksKolom(indeks));
    }

    private int toInt(Object v) {
        return v == null ? 0 : (Integer) v;
    }

    private Object nilai(int idxData) {
        if (kursor < 0 || kursor >= baris.size()) {
            throw new IllegalStateException(
                    "kursor tidak berada pada baris yang valid (panggil next() lebih dulu)");
        }
        Object v = baris.get(kursor)[idxData];
        nullTerakhir = (v == null);
        return v;
    }

    private int indeksKolom(int indeks) {
        if (indeks < 1 || indeks > kolom.size()) {
            throw new IllegalArgumentException(
                    "indeks kolom tidak valid: " + indeks
                            + " (jumlah kolom: " + kolom.size() + ")");
        }
        return indeks - 1; // kontrak JDBC: indeks berbasis 1
    }

    private int indeksKolom(String label) {
        int idx = kolom.indexOf(label);
        if (idx < 0) {
            throw new IllegalArgumentException("kolom tidak ditemukan: " + label);
        }
        return idx;
    }
}

/** "Database" demo dengan dua kueri yang dikenal. */
class DemoDb {

    static HasilKueri query(String sql) {
        List<Object[]> semua = List.of(
                new Object[]{1, "Buku Java", 150_000.00, 12},
                new Object[]{2, "Mouse", 250_000.00, null},
                new Object[]{3, "Keyboard", 450_000.00, 7});
        if (sql.equals("SELECT id, nama, harga, stok FROM produk")) {
            return new HasilKueri(List.of("id", "nama", "harga", "stok"), semua);
        }
        if (sql.equals("SELECT id, nama FROM produk")) {
            return new HasilKueri(List.of("id", "nama"), semua);
        }
        throw new IllegalArgumentException("kueri tidak dikenal: " + sql);
    }
}