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:
| Pendekatan | Berlaku di |
|---|---|
getGeneratedKeys() | standar JDBC, didukung SQLite, MySQL, PostgreSQL |
INSERT ... RETURNING id | sintaks 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:
== 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:
tabel.insert(...)mengembalikan1lalu2:AtomicIntegermulai dari nol, danincrementAndGet()menaikkan lalu mengembalikan nilai baru — dua pemanggilan menghasilkan dua id berurutan tanpa bisa saling menimpa.kunci : 3darigetGeneratedKeys(): statement dibuat dengancreateStatement(true)— padananprepareStatement(sql, Statement.RETURN_GENERATED_KEYS)— sehingga setiapINSERTyang lewat statement itu menyimpan id hasilnya.KunciHasildibaca dengan polaResultSet:next()menggeser kursor,getLong(1)mengambil satu-satunya kolom.kunci batch : 4dankunci batch : 5:getGeneratedKeys()setelahexecuteBatch()mengembalikan satu baris per pernyataan batch, urut sesuai antrian — jalur yang tepat untuk memetakan baris hasil impor ke id masing-masing.st3ditolak: statement yang dibuat tanpa permintaan kunci tidak punya kanal jawaban; pada JDBC asli, kondisi ini umumnya berakhir sebagaiSQLException. Kuncinya harus diminta sejak awal, bukan belakangan.getLong(1)memakai indeks1: hasilgetGeneratedKeys()mengikuti kontrakResultSet— 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):
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 #
- Oracle: Statement — getGeneratedKeys (JDK 25)
- Oracle: PreparedStatement (JDK 25)
- Oracle: package java.sql (JDK 25)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KunciOtomatis.java
Unduh berkasimport 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
}
}