Saat kode aplikasi tumbuh, string SQL punya kebiasaan buruk: menyebar. Satu method service membuka Connection, satu method lain menulis SELECT yang hampir sama, method ketiga menyalin blok try-with-resources — dan tiba-tiba mengganti database, atau sekadar menguji logika bisnis tanpa server, menjadi pekerjaan arkeologi. Pola DAO (Data Access Object) adalah jawaban paling klasik dan masih paling sering dipakai: semua akses data untuk satu entitas dikumpulkan ke satu class di belakang satu interface, sementara logika bisnis cukup mengenal method bermakna seperti simpan() dan cariById(). Chapter ini membangun DAO lengkap untuk entitas Pengguna, lalu menunjukkan bentuk implementasi JDBC-nya.
Masalah SQL yang Bertebaran #
Bayangkan method pendaftaran pengguna yang langsung menjalankan JDBC: membuka koneksi, menyiapkan PreparedStatement, mengikat parameter, membaca ResultSet, menutup resource. Kalau pola itu ditulis ulang di method login, pencarian, dan penghapusan, empat masalah muncul bersamaan. Pertama, duplikasi: blok boilerplate JDBC yang sama disalin ke mana-mana. Kedua, pengujian: menguji logika pendaftaran kini membutuhkan database hidup. Ketiga, kopling: keputusan skema tabel menular ke seluruh aplikasi — mengubah satu kolom berarti menyisir banyak berkas. Keempat, kesalahan resource: tiap salinan adalah tempat baru untuk lupa menutup Statement.
Anatomi Pola DAO #
DAO menyusun ulang kode menjadi tiga komponen dengan tanggung jawab tegas:
| Komponen | Wujud | Tanggung jawab |
|---|---|---|
| Transfer object | record Pengguna(...) | membawa data entitas antar lapisan |
| Interface DAO | PenggunaDao | kontrak operasi data: simpan, cari, ubah, hapus |
| Implementasi DAO | PenggunaDaoJdbc, PenggunaDaoMemori | mengerjakan kontrak dengan teknologi tertentu |
Alurnya berlapis: kode bisnis (service, controller) memanggil interface PenggunaDao tanpa tahu JDBC ada di belakangnya. Implementasi DAO-lah yang memegang Connection, PreparedStatement, dan ResultSet — seluruh JDBC terkurung di satu tempat. Transfer object memakai record karena pas secara semantik: pembawa data immutable dengan equals dan toString yang benar sejak lahir. Satu entitas, satu DAO — PenggunaDao untuk tabel pengguna, PesananDao untuk tabel pesanan; jangan membuat satu DAO raksasa untuk seluruh skema.
DAO dalam Praktik #
Program berikut memuat ketiga komponen: record Pengguna, interface PenggunaDao, dan implementasi PenggunaDaoMemori di atas LinkedHashMap. Operasi simpan meniru kolom identitas dengan AtomicLong, dan cariById mengembalikan Optional — pilihan yang memaksa pemanggil menyadari kemungkinan data tidak ditemukan:
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
public class AplikasiDao {
public static void main(String[] args) {
PenggunaDao dao = new PenggunaDaoMemori();
// Create
Pengguna budi = dao.simpan(new Pengguna(0, "Budi", "[email protected]"));
Pengguna siti = dao.simpan(new Pengguna(0, "Siti", "[email protected]"));
Pengguna dewi = dao.simpan(new Pengguna(0, "Dewi", "[email protected]"));
System.out.println("simpan : " + budi + ", " + siti + ", " + dewi);
// Read
System.out.println("cari 1 : " + dao.cariById(1).orElse(null));
System.out.println("cari 99 : " + dao.cariById(99));
System.out.println("semua : " + dao.semua());
// Update
boolean ubah = dao.perbarui(2, new Pengguna(2, "Siti A.", "[email protected]"));
System.out.println("perbarui: " + ubah + " -> " + dao.cariById(2).orElse(null));
// Delete
boolean hapus = dao.hapus(3);
System.out.println("hapus : " + ubahTeks(hapus) + " -> jumlah sekarang: "
+ dao.semua().size());
}
static String ubahTeks(boolean b) {
return b ? "berhasil" : "gagal";
}
}
/** Transfer object: membawa data antara aplikasi dan DAO. */
record Pengguna(long id, String nama, String email) {
}
/** Kontrak akses data untuk entitas Pengguna. */
interface PenggunaDao {
Pengguna simpan(Pengguna p); // INSERT
Optional<Pengguna> cariById(long id); // SELECT ... WHERE id = ?
List<Pengguna> semua(); // SELECT seluruh baris
boolean perbarui(long id, Pengguna pBaru); // UPDATE ... WHERE id = ?
boolean hapus(long id); // DELETE ... WHERE id = ?
}
/** Implementasi in-memory: pengganti tabel database saat belajar. */
class PenggunaDaoMemori implements PenggunaDao {
private final Map<Long, Pengguna> tabel = new LinkedHashMap<>();
private final AtomicLong urut = new AtomicLong();
@Override
public Pengguna simpan(Pengguna p) {
long id = urut.incrementAndGet();
Pengguna tersimpan = new Pengguna(id, p.nama(), p.email());
tabel.put(id, tersimpan);
return tersimpan;
}
@Override
public Optional<Pengguna> cariById(long id) {
return Optional.ofNullable(tabel.get(id));
}
@Override
public List<Pengguna> semua() {
return new ArrayList<>(tabel.values());
}
@Override
public boolean perbarui(long id, Pengguna pBaru) {
return tabel.replace(id, pBaru) != null;
}
@Override
public boolean hapus(long id) {
return tabel.remove(id) != null;
}
}Output yang Dihasilkan:
simpan : Pengguna[id=1, nama=Budi, [email protected]], Pengguna[id=2, nama=Siti, [email protected]], Pengguna[id=3, nama=Dewi, [email protected]]
cari 1 : Pengguna[id=1, nama=Budi, [email protected]]
cari 99 : Optional.empty
semua : [Pengguna[id=1, nama=Budi, [email protected]], Pengguna[id=2, nama=Siti, [email protected]], Pengguna[id=3, nama=Dewi, [email protected]]]
perbarui: true -> Pengguna[id=2, nama=Siti A., [email protected]]
hapus : berhasil -> jumlah sekarang: 2Mari kita telaah keputusan desain pada program di atas:
AplikasiDaohanya mengenalPenggunaDao: methodmainmemegang variabel bertipe interface, bukan class implementasi. MenggantiPenggunaDaoMemoridenganPenggunaDaoJdbctidak mengubah satu baris kode bisnis — hanya satu titik konstruksi.record Penggunasebagai transfer object:mainmencetak objek langsung dan hasilnya rapi (Pengguna[id=1, nama=Budi, ...]) —toStringbawaan record. OperasisimpanmengembalikanPenggunabaru dengan id terisi, meniru generated keys dari Chapter sebelumnya.OptionalpadacariById: id99tidak ada dan hasil cetaknyaOptional.empty— ketiadaan data menjadi nilai eksplisit yang diproses pemanggil, bukannullyang mengintip di tempat gelap.tabel.replace(id, pBaru)dantabel.remove(id): keduanya mengembalikan nilai lama ataunull, sehingga kontrak booleanperbarui/hapusterpenuhi tanpa pemeriksaan tambahan — keberhasilan diukur dari adanya data, bukan dari jumlah baris (pada JDBC asli, hasilexecuteUpdate() > 0berperan sama).LinkedHashMapmenjaga urutan sisipan: daftarsemua()selalu tampil dalam urutan Budi, Siti, Dewi — padananORDER BY idpada tabel sungguhan yang membuat output deterministik.
Implementasi JDBC dari Kontrak yang Sama #
Implementasi JDBC dari interface yang sama memegang seluruh kode yang tadinya kita khawatirkan menyebar. Bentuknya seperti berikut (ditampilkan sebagai contoh karena driver database tidak terpasang di lingkungan uji):
class PenggunaDaoJdbc implements PenggunaDao {
private final DataSource ds; // dari pool, lihat Chapter Connection Pooling
PenggunaDaoJdbc(DataSource ds) {
this.ds = ds;
}
@Override
public Optional<Pengguna> cariById(long id) {
String sql = "SELECT id, nama, email FROM pengguna WHERE id = ?";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next()
? Optional.of(new Pengguna(rs.getLong("id"),
rs.getString("nama"), rs.getString("email")))
: Optional.empty();
}
} catch (SQLException e) {
throw new DataAccessException("gagal mencari pengguna " + id, e);
}
}
@Override
public Pengguna simpan(Pengguna p) {
String sql = "INSERT INTO pengguna (nama, email) VALUES (?, ?)";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, p.nama());
ps.setString(2, p.email());
ps.executeUpdate();
try (ResultSet kunci = ps.getGeneratedKeys()) {
kunci.next();
return new Pengguna(kunci.getLong(1), p.nama(), p.email());
}
} catch (SQLException e) {
throw new DataAccessException("gagal menyimpan pengguna", e);
}
}
// perbarui() dan hapus() mengikuti pola yang sama
}
class DataAccessException extends RuntimeException {
DataAccessException(String pesan, Throwable sebab) {
super(pesan, sebab);
}
}Kekuatan pola ini paling terasa saat menguji: PenggunaDaoMemori pada program kita adalah persis implementasi palsu (test double) yang dipakai unit test — logika bisnis diuji tanpa server, cepat, dan deterministik, sementara PenggunaDaoJdbc diuji terpisah dengan database sungguhan. Namun DAO punya kritik klasik: kontraknya berorientasi baris tabel (cariById, semua), sedangkan pertanyaan bisnis sering berbentuk lain — “pengguna aktif mana yang belum membayar tagihan bulan ini?”. Chapter berikutnya memperkenalkan kerabatnya yang berorientasi domain: pola Repository.
Referensi #
- Oracle: package java.sql (JDK 25)
- Oracle: PreparedStatement (JDK 25)
- Oracle: javax.sql.DataSource (JDK 25)
- dev.java — Learn Java
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AplikasiDao.java
Unduh berkasimport java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
public class AplikasiDao {
public static void main(String[] args) {
PenggunaDao dao = new PenggunaDaoMemori();
// Create
Pengguna budi = dao.simpan(new Pengguna(0, "Budi", "[email protected]"));
Pengguna siti = dao.simpan(new Pengguna(0, "Siti", "[email protected]"));
Pengguna dewi = dao.simpan(new Pengguna(0, "Dewi", "[email protected]"));
System.out.println("simpan : " + budi + ", " + siti + ", " + dewi);
// Read
System.out.println("cari 1 : " + dao.cariById(1).orElse(null));
System.out.println("cari 99 : " + dao.cariById(99));
System.out.println("semua : " + dao.semua());
// Update
boolean ubah = dao.perbarui(2, new Pengguna(2, "Siti A.", "[email protected]"));
System.out.println("perbarui: " + ubah + " -> " + dao.cariById(2).orElse(null));
// Delete
boolean hapus = dao.hapus(3);
System.out.println("hapus : " + ubahTeks(hapus) + " -> jumlah sekarang: "
+ dao.semua().size());
}
static String ubahTeks(boolean b) {
return b ? "berhasil" : "gagal";
}
}
/** Transfer object: membawa data antara aplikasi dan DAO. */
record Pengguna(long id, String nama, String email) {
}
/** Kontrak akses data untuk entitas Pengguna. */
interface PenggunaDao {
Pengguna simpan(Pengguna p); // INSERT
Optional<Pengguna> cariById(long id); // SELECT ... WHERE id = ?
List<Pengguna> semua(); // SELECT seluruh baris
boolean perbarui(long id, Pengguna pBaru); // UPDATE ... WHERE id = ?
boolean hapus(long id); // DELETE ... WHERE id = ?
}
/** Implementasi in-memory: pengganti tabel database saat belajar. */
class PenggunaDaoMemori implements PenggunaDao {
private final Map<Long, Pengguna> tabel = new LinkedHashMap<>();
private final AtomicLong urut = new AtomicLong();
@Override
public Pengguna simpan(Pengguna p) {
long id = urut.incrementAndGet();
Pengguna tersimpan = new Pengguna(id, p.nama(), p.email());
tabel.put(id, tersimpan);
return tersimpan;
}
@Override
public Optional<Pengguna> cariById(long id) {
return Optional.ofNullable(tabel.get(id));
}
@Override
public List<Pengguna> semua() {
return new ArrayList<>(tabel.values());
}
@Override
public boolean perbarui(long id, Pengguna pBaru) {
return tabel.replace(id, pBaru) != null;
}
@Override
public boolean hapus(long id) {
return tabel.remove(id) != null;
}
}