Kita memasukkan produk baru ke tabel, lalu harus menyimpan rincian produk itu di tabel lain — tabel rincian_pesanan butuh produk_id sebagai foreign key. Persoalannya: kolom id diisi otomatis oleh database (identity / auto-increment), jadi program tidak tahu angka berapa yang baru saja diberikan. Solusi naif adalah SELECT id FROM produk WHERE nama = ? — rawan balasan lebih dari satu baris dan boros satu roundtrip. JDBC punya jalur resmi: minta generated keys saat membuat statement, lalu baca hasilnya lewat getGeneratedKeys(). Chapter ini membahas mekanisme penomoran otomatis di sisi database dan membuktikan alur pengambilannya.

Kolom Identitas di Sisi Database #

Kolom identitas dideklarasikan agar database mengisi nilainya sendiri setiap kali baris baru ditambahkan — SQLite memakai INTEGER PRIMARY KEY (bernama rowid), PostgreSQL memakai GENERATED ALWAYS AS IDENTITY, MySQL memakai AUTO_INCREMENT. Mekanisme internalnya berupa penghitung yang dinaikkan secara atomik: dua INSERT yang datang nyaris bersamaan tetap mendapat angka berbeda, dan angka yang sudah terpakai tidak dipakai ulang meskipun barisnya dihapus. Di Java, padanan mekanisme ini adalah AtomicInteger.incrementAndGet() — penambahan yang tak terpecahkan meski dipanggil dari banyak thread.

Cara mengambil kunci yang barusan tercetak bervariasi antar database, dan perbedaan itulah yang JDBC satukan:

PendekatanBerlaku di
getGeneratedKeys()standar JDBC, didukung SQLite, MySQL, PostgreSQL
INSERT ... RETURNING idsintaks SQL asli PostgreSQL (dan SQLite modern)
LAST_INSERT_ID() / last_insert_rowid()fungsi sesi MySQL / SQLite
SELECT currval(...)sequence PostgreSQL

Mekanisme RETURN_GENERATED_KEYS di JDBC #

Permintaan kunci disampaikan saat statement dibuat, bukan saat eksekusi: prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) memberi tahu driver agar menyiapkan kanal jawaban tambahan sebelum INSERT dikirim. Setelah executeUpdate berhasil, getGeneratedKeys() mengembalikan sebuah ResultSet mini berisi kolom kunci — satu baris untuk satu INSERT, sebanyak itu pula baris untuk isi batch. Karena bentuknya ResultSet, membacanya memakai pola yang sudah kita kuasai: next() untuk memindahkan kursor, getLong(1) untuk mengambil nilai.

Simulasi Penomoran dan Pengambilan Kunci #

Program berikut meniru kolom identitas dengan AtomicInteger, lalu menyajikan dua jalur pengambilan: nilai kembalian insert() (padanan RETURNING) dan getGeneratedKeys() pada StatementMini (padanan JDBC). Skenario terakhir membuktikan penolakan getGeneratedKeys() ketika statement dibuat tanpa permintaan kunci:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class KunciOtomatis {

    public static void main(String[] args) {
        TabelMini tabel = new TabelMini();

        System.out.println("== INSERT satu per satu ==");
        System.out.println("id produk baru : " + tabel.insert("Buku Java", "150000"));
        System.out.println("id produk baru : " + tabel.insert("Mouse", "250000"));

        System.out.println();
        System.out.println("== getGeneratedKeys setelah INSERT ==");
        StatementMini st = tabel.createStatement(true);   // padanan RETURN_GENERATED_KEYS
        st.executeUpdate("INSERT INTO produk (nama, harga) VALUES ('Monitor', '1200000')");
        try (KunciHasil kunci = st.getGeneratedKeys()) {
            while (kunci.next()) {
                System.out.println("kunci          : " + kunci.getLong(1));
            }
        }

        System.out.println();
        System.out.println("== Kunci dari batch ==");
        StatementMini st2 = tabel.createStatement(true);
        st2.addBatch("INSERT INTO produk (nama, harga) VALUES ('Kabel HDMI', '75000')");
        st2.addBatch("INSERT INTO produk (nama, harga) VALUES ('Web Camera', '350000')");
        st2.executeBatch();
        try (KunciHasil kunci = st2.getGeneratedKeys()) {
            while (kunci.next()) {
                System.out.println("kunci batch    : " + kunci.getLong(1));
            }
        }

        System.out.println();
        System.out.println("== Statement tanpa permintaan kunci ==");
        StatementMini st3 = tabel.createStatement(false);
        try {
            st3.getGeneratedKeys();
        } catch (IllegalStateException e) {
            System.out.println("DITOLAK: " + e.getMessage());
        }

        System.out.println();
        System.out.println("== Isi tabel akhir ==");
        tabel.cetak();
    }
}

/** Tabel produk dengan kolom identitas: id naik otomatis tiap INSERT. */
class TabelMini {

    private final List<String[]> baris = new ArrayList<>();
    private final AtomicInteger urut = new AtomicInteger();

    long insert(String nama, String harga) {
        long id = urut.incrementAndGet();
        baris.add(new String[]{String.valueOf(id), nama, harga});
        return id;
    }

    StatementMini createStatement(boolean mintaKunci) {
        return new StatementMini(this, mintaKunci);
    }

    void cetak() {
        System.out.println("| id | nama       | harga   |");
        for (String[] b : baris) {
            System.out.printf("| %-2s | %-10s | %-7s |%n", b[0], b[1], b[2]);
        }
    }
}

/** Statement mini yang bisa "meminta kunci" saat dibuat. */
class StatementMini implements AutoCloseable {

    private final TabelMini tabel;
    private final boolean mintaKunci;
    private final List<String> antrian = new ArrayList<>();
    private final List<Long> kunci = new ArrayList<>();

    StatementMini(TabelMini tabel, boolean mintaKunci) {
        this.tabel = tabel;
        this.mintaKunci = mintaKunci;
    }

    int executeUpdate(String sql) {
        Matcher m = Pattern
                .compile("INSERT INTO produk \\(nama, harga\\) VALUES \\('([^']+)', '([^']+)'\\)")
                .matcher(sql);
        if (!m.matches()) {
            throw new IllegalArgumentException("SQL tidak dikenal: " + sql);
        }
        long id = tabel.insert(m.group(1), m.group(2));
        if (mintaKunci) {
            kunci.add(id);
        }
        return 1;
    }

    void addBatch(String sql) {
        antrian.add(sql);
    }

    int[] executeBatch() {
        System.out.println("[batch] " + antrian.size() + " pernyataan dieksekusi");
        int[] hasil = new int[antrian.size()];
        for (int i = 0; i < antrian.size(); i++) {
            hasil[i] = executeUpdate(antrian.get(i));
        }
        antrian.clear();
        return hasil;
    }

    KunciHasil getGeneratedKeys() {
        if (!mintaKunci) {
            throw new IllegalStateException(
                    "getGeneratedKeys() ditolak: statement dibuat tanpa permintaan kunci");
        }
        return new KunciHasil(new ArrayList<>(kunci));
    }

    @Override
    public void close() {
        // tidak ada resource nyata pada shim ini
    }
}

/** Padanan ResultSet kunci: satu kolom, kursor berbasis next(). */
class KunciHasil implements AutoCloseable {

    private final List<Long> kunci;
    private int kursor = -1;

    KunciHasil(List<Long> kunci) {
        this.kunci = kunci;
    }

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

    long getLong(int indeks) {
        if (indeks != 1) {
            throw new IllegalArgumentException("kolom kunci hanya ada satu");
        }
        if (kursor < 0 || kursor >= kunci.size()) {
            throw new IllegalStateException("kursor tidak berada pada baris yang valid");
        }
        return kunci.get(kursor);
    }

    @Override
    public void close() {
        // tidak ada resource nyata pada shim ini
    }
}

Output yang Dihasilkan:

text
== INSERT satu per satu ==
id produk baru : 1
id produk baru : 2

== getGeneratedKeys setelah INSERT ==
kunci          : 3

== Kunci dari batch ==
[batch] 2 pernyataan dieksekusi
kunci batch    : 4
kunci batch    : 5

== Statement tanpa permintaan kunci ==
DITOLAK: getGeneratedKeys() ditolak: statement dibuat tanpa permintaan kunci

== Isi tabel akhir ==
| id | nama       | harga   |
| 1  | Buku Java  | 150000  |
| 2  | Mouse      | 250000  |
| 3  | Monitor    | 1200000 |
| 4  | Kabel HDMI | 75000   |
| 5  | Web Camera | 350000  |

Mari kita telaah alur pada output di atas:

  1. tabel.insert(...) mengembalikan 1 lalu 2: AtomicInteger mulai dari nol, dan incrementAndGet() menaikkan lalu mengembalikan nilai baru — dua pemanggilan menghasilkan dua id berurutan tanpa bisa saling menimpa.
  2. kunci : 3 dari getGeneratedKeys(): statement dibuat dengan createStatement(true) — padanan prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) — sehingga setiap INSERT yang lewat statement itu menyimpan id hasilnya. KunciHasil dibaca dengan pola ResultSet: next() menggeser kursor, getLong(1) mengambil satu-satunya kolom.
  3. kunci batch : 4 dan kunci batch : 5: getGeneratedKeys() setelah executeBatch() mengembalikan satu baris per pernyataan batch, urut sesuai antrian — jalur yang tepat untuk memetakan baris hasil impor ke id masing-masing.
  4. st3 ditolak: statement yang dibuat tanpa permintaan kunci tidak punya kanal jawaban; pada JDBC asli, kondisi ini umumnya berakhir sebagai SQLException. Kuncinya harus diminta sejak awal, bukan belakangan.
  5. getLong(1) memakai indeks 1: hasil getGeneratedKeys() mengikuti kontrak ResultSet — indeks kolom berbasis satu, dan pada umumnya hanya ada satu kolom kunci.

Bentuk pada JDBC Sungguhan #

Rangkaian lengkap dengan SQLite — menyimpan produk lalu memakai id-nya untuk baris tabel lain — tampak seperti berikut (ditampilkan sebagai contoh karena driver tidak terpasang di lingkungan uji):

java INSERT lalu pakai id-nya (butuh driver SQLite)
String sql = "INSERT INTO produk (nama, harga) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:toko.db");
     PreparedStatement ps = conn.prepareStatement(sql,
             Statement.RETURN_GENERATED_KEYS)) {

    ps.setString(1, "Buku Java");
    ps.setBigDecimal(2, new BigDecimal("150000.00"));
    ps.executeUpdate();

    long idBaru;
    try (ResultSet kunci = ps.getGeneratedKeys()) {
        kunci.next();                       // tepat satu baris hasil
        idBaru = kunci.getLong(1);
    }
    System.out.println("Produk tersimpan dengan id " + idBaru);

    try (PreparedStatement rincian = conn.prepareStatement(
            "INSERT INTO rincian_pesanan (produk_id, jumlah) VALUES (?, ?)")) {
        rincian.setLong(1, idBaru);         // foreign key mengacu id baru
        rincian.setInt(2, 2);
        rincian.executeUpdate();
    }
}

Kunci primer otomatis kini bisa kita ambil tanpa menebak. Tetapi ada alasan mengapa kolom identitas patut dibahas berdampingan keamanan: nomor yang bisa ditebak bukan satu-satunya hal yang bisa disalahgunakan pengguna jahat. Chapter berikutnya membongkar anatomi serangan tertua di dunia database — SQL injection — beserta pertahanan berlapisnya.

Referensi #

Berkas Sumber

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

KunciOtomatis.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class KunciOtomatis {

    public static void main(String[] args) {
        TabelMini tabel = new TabelMini();

        System.out.println("== INSERT satu per satu ==");
        System.out.println("id produk baru : " + tabel.insert("Buku Java", "150000"));
        System.out.println("id produk baru : " + tabel.insert("Mouse", "250000"));

        System.out.println();
        System.out.println("== getGeneratedKeys setelah INSERT ==");
        StatementMini st = tabel.createStatement(true);   // padanan RETURN_GENERATED_KEYS
        st.executeUpdate("INSERT INTO produk (nama, harga) VALUES ('Monitor', '1200000')");
        try (KunciHasil kunci = st.getGeneratedKeys()) {
            while (kunci.next()) {
                System.out.println("kunci          : " + kunci.getLong(1));
            }
        }

        System.out.println();
        System.out.println("== Kunci dari batch ==");
        StatementMini st2 = tabel.createStatement(true);
        st2.addBatch("INSERT INTO produk (nama, harga) VALUES ('Kabel HDMI', '75000')");
        st2.addBatch("INSERT INTO produk (nama, harga) VALUES ('Web Camera', '350000')");
        st2.executeBatch();
        try (KunciHasil kunci = st2.getGeneratedKeys()) {
            while (kunci.next()) {
                System.out.println("kunci batch    : " + kunci.getLong(1));
            }
        }

        System.out.println();
        System.out.println("== Statement tanpa permintaan kunci ==");
        StatementMini st3 = tabel.createStatement(false);
        try {
            st3.getGeneratedKeys();
        } catch (IllegalStateException e) {
            System.out.println("DITOLAK: " + e.getMessage());
        }

        System.out.println();
        System.out.println("== Isi tabel akhir ==");
        tabel.cetak();
    }
}

/** Tabel produk dengan kolom identitas: id naik otomatis tiap INSERT. */
class TabelMini {

    private final List<String[]> baris = new ArrayList<>();
    private final AtomicInteger urut = new AtomicInteger();

    long insert(String nama, String harga) {
        long id = urut.incrementAndGet();
        baris.add(new String[]{String.valueOf(id), nama, harga});
        return id;
    }

    StatementMini createStatement(boolean mintaKunci) {
        return new StatementMini(this, mintaKunci);
    }

    void cetak() {
        System.out.println("| id | nama       | harga   |");
        for (String[] b : baris) {
            System.out.printf("| %-2s | %-10s | %-7s |%n", b[0], b[1], b[2]);
        }
    }
}

/** Statement mini yang bisa "meminta kunci" saat dibuat. */
class StatementMini implements AutoCloseable {

    private final TabelMini tabel;
    private final boolean mintaKunci;
    private final List<String> antrian = new ArrayList<>();
    private final List<Long> kunci = new ArrayList<>();

    StatementMini(TabelMini tabel, boolean mintaKunci) {
        this.tabel = tabel;
        this.mintaKunci = mintaKunci;
    }

    int executeUpdate(String sql) {
        Matcher m = Pattern
                .compile("INSERT INTO produk \\(nama, harga\\) VALUES \\('([^']+)', '([^']+)'\\)")
                .matcher(sql);
        if (!m.matches()) {
            throw new IllegalArgumentException("SQL tidak dikenal: " + sql);
        }
        long id = tabel.insert(m.group(1), m.group(2));
        if (mintaKunci) {
            kunci.add(id);
        }
        return 1;
    }

    void addBatch(String sql) {
        antrian.add(sql);
    }

    int[] executeBatch() {
        System.out.println("[batch] " + antrian.size() + " pernyataan dieksekusi");
        int[] hasil = new int[antrian.size()];
        for (int i = 0; i < antrian.size(); i++) {
            hasil[i] = executeUpdate(antrian.get(i));
        }
        antrian.clear();
        return hasil;
    }

    KunciHasil getGeneratedKeys() {
        if (!mintaKunci) {
            throw new IllegalStateException(
                    "getGeneratedKeys() ditolak: statement dibuat tanpa permintaan kunci");
        }
        return new KunciHasil(new ArrayList<>(kunci));
    }

    @Override
    public void close() {
        // tidak ada resource nyata pada shim ini
    }
}

/** Padanan ResultSet kunci: satu kolom, kursor berbasis next(). */
class KunciHasil implements AutoCloseable {

    private final List<Long> kunci;
    private int kursor = -1;

    KunciHasil(List<Long> kunci) {
        this.kunci = kunci;
    }

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

    long getLong(int indeks) {
        if (indeks != 1) {
            throw new IllegalArgumentException("kolom kunci hanya ada satu");
        }
        if (kursor < 0 || kursor >= kunci.size()) {
            throw new IllegalStateException("kursor tidak berada pada baris yang valid");
        }
        return kunci.get(kursor);
    }

    @Override
    public void close() {
        // tidak ada resource nyata pada shim ini
    }
}