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:

KomponenWujudTanggung jawab
Transfer objectrecord Pengguna(...)membawa data entitas antar lapisan
Interface DAOPenggunaDaokontrak operasi data: simpan, cari, ubah, hapus
Implementasi DAOPenggunaDaoJdbc, PenggunaDaoMemorimengerjakan 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:

java AplikasiDao.java
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:

text
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: 2

Mari kita telaah keputusan desain pada program di atas:

  1. AplikasiDao hanya mengenal PenggunaDao: method main memegang variabel bertipe interface, bukan class implementasi. Mengganti PenggunaDaoMemori dengan PenggunaDaoJdbc tidak mengubah satu baris kode bisnis — hanya satu titik konstruksi.
  2. record Pengguna sebagai transfer object: main mencetak objek langsung dan hasilnya rapi (Pengguna[id=1, nama=Budi, ...]) — toString bawaan record. Operasi simpan mengembalikan Pengguna baru dengan id terisi, meniru generated keys dari Chapter sebelumnya.
  3. Optional pada cariById: id 99 tidak ada dan hasil cetaknya Optional.empty — ketiadaan data menjadi nilai eksplisit yang diproses pemanggil, bukan null yang mengintip di tempat gelap.
  4. tabel.replace(id, pBaru) dan tabel.remove(id): keduanya mengembalikan nilai lama atau null, sehingga kontrak boolean perbarui/hapus terpenuhi tanpa pemeriksaan tambahan — keberhasilan diukur dari adanya data, bukan dari jumlah baris (pada JDBC asli, hasil executeUpdate() > 0 berperan sama).
  5. LinkedHashMap menjaga urutan sisipan: daftar semua() selalu tampil dalam urutan Budi, Siti, Dewi — padanan ORDER BY id pada 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):

java PenggunaDaoJdbc: kontrak yang sama, JDBC di dalamnya
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 #

Berkas Sumber

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

AplikasiDao.java

Unduh berkas
java
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;
    }
}