Perjalanan Section database panjang: dari arsitektur JDBC, koneksi, dan statement, hingga transaksi, pooling, dan dua pola abstraksi. Seluruh materi itu bermuara pada satu pertanyaan praktis — bagaimana menulis lapisan akses data yang benar sejak baris pertama, bukan setelah insiden produksi pertama. Chapter penutup ini merangkum praktik terbaik JDBC menjadi satu daftar periksa yang saling menguatkan, masing-masing dengan alasan teknis yang telah kita bedah di chapter sebelumnya.
Sumber Daya: Tutup Semua, Selalu #
Aturan tertua sekaligus terpenting: Connection, Statement, dan ResultSet semuanya mengadakan sumber daya asing dan wajib ditutup. Tanpa penutupan, koneksi bocor perlahan hingga pool habis dan aplikasi berhenti merespons. Bentuk modernnya tunggal dan tak terbantahkan — try-with-resources bertingkat, seperti pola berikut:
String sql = "SELECT nama, email FROM peserta WHERE kategori = ?";
try (Connection koneksi = sumber.getConnection();
PreparedStatement ps = koneksi.prepareStatement(sql)) {
ps.setString(1, kategori);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
hasil.add(petakanBaris(rs));
}
}
}Urutan penutupan berlangsung terbalik secara otomatis — ResultSet, lalu Statement, lalu Connection — dan berjalan juga saat exception lempar. Ketika pooling dipakai, close() pada koneksi pinjaman berarti mengembalikan ke pool, bukan memutus TCP; jadi kebiasaan menutup tetap benar di kedua dunia.
Statement: PreparedStatement Selalu #
Praktik kedua menutup pintu kerentanan paling terkenal di dunia database: SQL injection. Gabungkan string SQL dengan input pengguna dan satu tanda kutip cukup untuk mengubah makna kueri — Chapter Pencegahan SQL Injection telah membedah cara kerjanya. PreparedStatement memisahkan struktur SQL dari data: struktur dikirim sekali untuk dikompilasi, data mengisi slot ? sebagai nilai murni yang tidak akan pernah diartikan sebagai kode. Manfaat sampingannya tidak kalah besar: statement terkompilasi dapat dipakai ulang dengan parameter berbeda (hemat parse di server) dan mendukung batch. Kaidah praktisnya satu kalimat: jika ada input, tidak ada alasan memakai Statement polos.
Data: Petakan dengan Rapi dan Batasi yang Diambil #
Kolom database bukan field Java; jembatannya harus eksplisit. Baca dengan tipe yang tepat (getString, getLong, getInt) berdasarkan label kolom, bukan posisi angka yang rapuh terhadap SELECT *. Lebih baik lagi: tulis proyeksi eksplisit (SELECT nama, email alih-alih SELECT *) sehingga perubahan skema tidak menggeser makna kode secara diam-diam. Untuk tabel besar, batasi jumlah baris — klausa LIMIT/FETCH FIRST, fetchSize pada statement, dan pagination pada API publik menjaga satu kueri tidak memuat sejuta baris ke memori hanya untuk tiga baris pertama yang dibaca pengguna.
Transaksi dan Kegagalan: Pikirkan Batas dan Pemulihan #
Praktik keempat menyangkut kebenaran data. Kelompokkan operasi yang harus atomik — misalnya debet dan kredit transfer — dalam satu transaksi setAutoCommit(false) dengan commit() di jalur sukses dan rollback() di catch, seperti yang kita latih pada Chapter Transaksi dengan JDBC. Tetapkan setTransactionIsolation sesuai kebutuhan nyata; isolasi tertinggi bukan gratis. Di luar transaksi, siapkan strategi kegagalan: SQLException membawa getSQLState() dan getErrorCode() untuk membedakan kondisi sementara (koneksi terputus — layak dicoba ulang) dari kesalahan logika (constraint pelanggar — jangan diulang). Terakhir, kebijakan fail fast: timeout koneksi dan kueri yang eksplisit mencegah satu kueri lambat menahan seluruh pool.
Struktur: Sembunyikan JDBC di Balik Abstraksi #
Praktik kelima menjaga arsitektur tetap sehat. Seluruh kode JDBC — koneksi, statement, pemetaan baris — sebaiknya hidup di satu tempat: implementasi repository atau DAO, bukan tersebar di kode bisnis. Program kecil berikut memperlihatkan wujud akhirnya dari sisi konsumen: validasi di batas, objek domain bersih, dan tidak ada satu baris SQL di alur bisnis:
import java.util.ArrayList;
import java.util.List;
public class PraktikBaik {
record Peserta(long id, String nama, String email) {
String ringkas() {
return "#" + id + " " + nama + " <" + email + ">";
}
}
// Repository menyerap SQL; kode bisnis hanya melihat objek domain
static final class PesertaRepo {
private final List<Peserta> tabel = new ArrayList<>();
private long urut = 0;
Peserta simpan(String nama, String email) {
// input divalidasi DI BATAS -- bukan di SQL
if (nama == null || nama.isBlank() || !email.endsWith("@example.com")) {
throw new IllegalArgumentException("data peserta tidak sah");
}
Peserta baru = new Peserta(++urut, nama.strip(), email.toLowerCase());
tabel.add(baru);
return baru;
}
List<Peserta> cariNama(String kata) {
List<Peserta> hasil = new ArrayList<>();
for (Peserta p : tabel) {
if (p.nama().toLowerCase().contains(kata.toLowerCase())) {
hasil.add(p);
}
}
return hasil;
}
}
public static void main(String[] args) {
PesertaRepo repo = new PesertaRepo();
repo.simpan("Budi Santoso", "[email protected]");
repo.simpan("Siti Rahayu", "[email protected]");
repo.simpan("Dewi Lestari", "[email protected]");
System.out.println("cari 'bu' : " + ringkas(repo.cariNama("bu")));
System.out.println("cari 'ti' : " + ringkas(repo.cariNama("ti")));
try {
repo.simpan("Salah", "bukan-email");
} catch (IllegalArgumentException e) {
System.out.println("ditolak : " + e.getMessage());
}
}
static List<String> ringkas(List<Peserta> daftar) {
List<String> r = new ArrayList<>();
for (Peserta p : daftar) {
r.add(p.ringkas());
}
return r;
}
}Output yang Dihasilkan:
cari 'bu' : [#1 Budi Santoso <[email protected]>]
cari 'ti' : [#2 Siti Rahayu <[email protected]>]
ditolak : data peserta tidak sahPerhatikan tiga hal pada program tersebut. Pertama, validasi input terjadi sebelum data menyentuh lapisan penyimpanan — data sampah ditolak di pintu. Kedua, normalisasi (strip(), toLowerCase()) dilakukan sekali di titik masuk sehingga seluruh kueri bekerja pada data yang sudah bersih. Ketiga, penolakan terjadi sebagai exception domain yang bermakna, bukan kegagalan SQL misterius — pemanggil bisa menanggapi dengan tepat.
Daftar Periksa Penutup #
Rangkuman Section ini dalam satu tabel yang layak dicetak:
| Area | Praktik | Penangkal |
|---|---|---|
| Resource | try-with-resources bertingkat | koneksi bocor, pool habis |
| Statement | PreparedStatement + slot ? | SQL injection |
| Kueri | proyeksi eksplisit, label kolom, LIMIT/fetchSize | memori membengkak, kode rapuh |
| Transaksi | batas eksplisit, commit/rollback berpasangan | saldo tidak konsisten |
| Kegagalan | timeout, klasifikasi SQLState, retry hanya kondisi sementara | hang & badai retry |
| Struktur | JDBC hanya di repository/DAO + pool (HikariCP) | kode tersebar, sulit diuji |
Dengan daftar ini, lapisan data aplikasi Java Anda berdiri di atas fondasi yang sama dengan sistem produksi kelas dunia. Section berikutnya meninggalkan database dan membuka dua kemampuan yang membuat framework modern bekerja: annotations dan reflection.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PraktikBaik.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class PraktikBaik {
record Peserta(long id, String nama, String email) {
String ringkas() {
return "#" + id + " " + nama + " <" + email + ">";
}
}
// Repository menyerap SQL; kode bisnis hanya melihat objek domain
static final class PesertaRepo {
private final List<Peserta> tabel = new ArrayList<>();
private long urut = 0;
Peserta simpan(String nama, String email) {
// input divalidasi DI BATAS -- bukan di SQL
if (nama == null || nama.isBlank() || !email.endsWith("@example.com")) {
throw new IllegalArgumentException("data peserta tidak sah");
}
Peserta baru = new Peserta(++urut, nama.strip(), email.toLowerCase());
tabel.add(baru);
return baru;
}
List<Peserta> cariNama(String kata) {
List<Peserta> hasil = new ArrayList<>();
for (Peserta p : tabel) {
if (p.nama().toLowerCase().contains(kata.toLowerCase())) {
hasil.add(p);
}
}
return hasil;
}
}
public static void main(String[] args) {
PesertaRepo repo = new PesertaRepo();
repo.simpan("Budi Santoso", "[email protected]");
repo.simpan("Siti Rahayu", "[email protected]");
repo.simpan("Dewi Lestari", "[email protected]");
System.out.println("cari 'bu' : " + ringkas(repo.cariNama("bu")));
System.out.println("cari 'ti' : " + ringkas(repo.cariNama("ti")));
try {
repo.simpan("Salah", "bukan-email");
} catch (IllegalArgumentException e) {
System.out.println("ditolak : " + e.getMessage());
}
}
static List<String> ringkas(List<Peserta> daftar) {
List<String> r = new ArrayList<>();
for (Peserta p : daftar) {
r.add(p.ringkas());
}
return r;
}
}