Chapter sebelumnya memisahkan akses data ke dalam object DAO, namun DAO tetap sering berbau meja database: method seperti insertRow, updateById, atau executeQuery menuntut pemanggil berpikir dalam istilah baris dan SQL. Pattern Repository melangkah lebih jauh: ia menyajikan koleksi objek domain seolah-olah sudah berada di memori, sehingga kode bisnis cukup memanggil cariByKategori("buku") tanpa peduli bahwa di baliknya ada database, berkas, atau layanan web. Pada Chapter ini kita akan membangun repository dari kontraknya, membandingkannya dengan DAO, dan melihat kenapa pola ini menjadi fondasi Spring Data JPA.
Kontrak Berorientasi Domain #
Repository didefinisikan sebagai interface yang berbicara bahasa domain: entitas bernama bermakna, method kueri yang menjawab pertanyaan bisnis, dan nilai kembalian berupa objek domain atau Optional. Interface berikut memodelkan penyimpanan produk:
record Produk(long id, String nama, String kategori, int harga) {
}
interface ProdukRepository {
Produk tambah(Produk p);
List<Produk> semua();
Optional<Produk> cariById(long id);
List<Produk> cariByKategori(String kategori);
List<Produk> cariHargaDiBawah(int batas);
List<Produk> cariKataKunci(String kata);
boolean hapus(Produk p);
}
class ProdukRepositoryMemori implements ProdukRepository {
private final Map<Long, Produk> data = new LinkedHashMap<>();
private final AtomicLong urutId = new AtomicLong();
@Override
public Produk tambah(Produk p) {
long idBaru = urutId.incrementAndGet();
Produk tersimpan = new Produk(idBaru, p.nama(), p.kategori(), p.harga());
data.put(idBaru, tersimpan);
return tersimpan;
}
@Override
public List<Produk> semua() {
return List.copyOf(data.values());
}
@Override
public Optional<Produk> cariById(long id) {
return Optional.ofNullable(data.get(id));
}
@Override
public List<Produk> cariByKategori(String kategori) {
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.kategori().equals(kategori)) {
hasil.add(p);
}
}
return hasil;
}
@Override
public List<Produk> cariHargaDiBawah(int batas) {
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.harga() < batas) {
hasil.add(p);
}
}
return hasil;
}
@Override
public List<Produk> cariKataKunci(String kata) {
String kunci = kata.toLowerCase(Locale.ROOT);
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.nama().toLowerCase(Locale.ROOT).contains(kunci)) {
hasil.add(p);
}
}
return hasil;
}
@Override
public boolean hapus(Produk p) {
return data.remove(p.id(), p);
}
}Berkas di atas sengaja memuat kontrak dan implementasi sekaligus agar mudah dibaca utuh. Mari kita bedah bagian-bagiannya:
- Baris awal (
record Produk(...)): entitas domain dibuat immutable denganrecord; identitas produk ditentukanid, bukan seluruh isinya. - Interface
ProdukRepository: seluruh method memakai bahasa domain —cariByKategori,cariHargaDiBawah,cariKataKunci— bukan bahasa SQL sepertiselectWhere. tambah(...): repository yang bertanggung jawab menetapkan identitas; pemanggil mengirim produk tanpa id (0) dan menerima salinan tersimpan dengan id resmi.semua()mengembalikanList.copyOf(...): salinan tak dapat diubah mencegah pemanggil mengutak-atik isi penyimpanan dari luar — penyimpanan tetap satu-satunya sumber kebenaran.hapus(...)denganMap.remove(key, value): penghapusan hanya berhasil bila objek di penyimpanan masih identik dengan yang dipegang pemanggil — semantik koleksi yang jujur.
Repository dalam Aksi #
Program berikut memakai repository dari sisi konsumen. Perhatikan bahwa seluruh interaksi terasa seperti bekerja dengan koleksi biasa:
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
public class AplikasiRepository {
public static void main(String[] args) {
ProdukRepository repo = new ProdukRepositoryMemori();
System.out.println("== Mengisi repository ==");
repo.tambah(new Produk(0, "Buku Java Dasar", "buku", 150_000));
repo.tambah(new Produk(0, "Mouse Wireless", "elektronik", 250_000));
repo.tambah(new Produk(0, "Buku SQL Praktis", "buku", 180_000));
repo.tambah(new Produk(0, "Pensil Mekanik", "alat-tulis", 75_000));
Produk keyboard = repo.tambah(new Produk(0, "Keyboard Compact", "elektronik", 450_000));
System.out.println("produk terdaftar : " + repo.semua().size());
System.out.println();
System.out.println("== Kueri bermakna domain ==");
System.out.println("cariByKategori(\"buku\") : " + ringkas(repo.cariByKategori("buku")));
System.out.println("cariHargaDiBawah(200000) : " + ringkas(repo.cariHargaDiBawah(200_000)));
System.out.println("cariKataKunci(\"java\") : " + ringkas(repo.cariKataKunci("java")));
System.out.println();
System.out.println("== Semantik koleksi ==");
System.out.println("hapus(keyboard) : " + repo.hapus(keyboard)
+ " -> sisa: " + repo.semua().size());
System.out.println("cariById(5) : " + repo.cariById(5));
}
static List<String> ringkas(List<Produk> daftar) {
List<String> hasil = new ArrayList<>();
for (Produk p : daftar) {
hasil.add("#" + p.id() + " " + p.nama() + " (" + p.harga() + ")");
}
return hasil;
}
}
/** Entitas domain: kaya makna bisnis, bukan cerminan kolom tabel. */
record Produk(long id, String nama, String kategori, long harga) {
}
/** Kontrak repository: bahasa domain, bukan bahasa tabel. */
interface ProdukRepository {
Produk tambah(Produk p);
Optional<Produk> cariById(long id);
List<Produk> semua();
List<Produk> cariByKategori(String kategori);
List<Produk> cariHargaDiBawah(long batas);
List<Produk> cariKataKunci(String kata);
boolean hapus(Produk p);
}
/** Implementasi in-memory: koleksi objek domain sebagai sumber data. */
class ProdukRepositoryMemori implements ProdukRepository {
private final Map<Long, Produk> data = new LinkedHashMap<>();
private final AtomicLong urut = new AtomicLong();
@Override
public Produk tambah(Produk p) {
long id = urut.incrementAndGet();
Produk tersimpan = new Produk(id, p.nama(), p.kategori(), p.harga());
data.put(id, tersimpan);
return tersimpan;
}
@Override
public Optional<Produk> cariById(long id) {
return Optional.ofNullable(data.get(id));
}
@Override
public List<Produk> semua() {
return new ArrayList<>(data.values());
}
@Override
public List<Produk> cariByKategori(String kategori) {
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.kategori().equals(kategori)) {
hasil.add(p);
}
}
return hasil;
}
@Override
public List<Produk> cariHargaDiBawah(long batas) {
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.harga() < batas) {
hasil.add(p);
}
}
return hasil;
}
@Override
public List<Produk> cariKataKunci(String kata) {
String k = kata.toLowerCase(Locale.ROOT);
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.nama().toLowerCase(Locale.ROOT).contains(k)) {
hasil.add(p);
}
}
return hasil;
}
@Override
public boolean hapus(Produk p) {
return data.remove(p.id(), p); // hanya bila entitas yang sama pula tersimpan
}
}Output yang Dihasilkan:
== Mengisi repository ==
produk terdaftar : 5
== Kueri bermakna domain ==
cariByKategori("buku") : [#1 Buku Java Dasar (150000), #3 Buku SQL Praktis (180000)]
cariHargaDiBawah(200000) : [#1 Buku Java Dasar (150000), #3 Buku SQL Praktis (180000), #4 Pensil Mekanik (75000)]
cariKataKunci("java") : [#1 Buku Java Dasar (150000)]
== Semantik koleksi ==
hapus(keyboard) : true -> sisa: 4
cariById(5) : Optional.emptyTidak ada satu baris SQL pun di kode pemanggil. Ketika nanti implementasi berganti ke JDBC, Hibernate, atau REST API, method main di atas tidak berubah — inilah keuntungan nyata bergantung pada abstraksi, bukan pada detail.
Repository vs DAO #
Kedua pola sering dianggap sama, padahal sudut pandangnya berbeda. DAO berorientasi meja: satu class per tabel dengan operasi baris, cocok ketika pemetaan objek-tabel masih sederhana. Repository berorientasi domain: satu repository per aggregate yang bisa mencakup beberapa tabel, dengan kueri yang menjawab pertanyaan bisnis. DAO menjawab “bagaimana baris disimpan”, repository menjawab “bagaimana domain menemukan objeknya”. Pada aplikasi ber-domain kaya, repository biasanya berada di atas DAO atau langsung di atas ORM. Spring Data JPA mengambil pola ini sebagai warganya: kita cukup mendeklarasikan interface ProdukRepository extends JpaRepository<Produk, Long> beserta method kueri bermakna, dan framework yang menyediakan implementasinya.
Dengan repository, lapisan akses data kini berbicara bahasa aplikasi. Chapter selanjutnya membahas satu persoalan yang tetap tak terselesaikan oleh pola mana pun: bagaimana menjaga skema database tetap selaras dengan kode saat aplikasi tumbuh, melalui database migrations.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AplikasiRepository.java
Unduh berkasimport java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
public class AplikasiRepository {
public static void main(String[] args) {
ProdukRepository repo = new ProdukRepositoryMemori();
System.out.println("== Mengisi repository ==");
repo.tambah(new Produk(0, "Buku Java Dasar", "buku", 150_000));
repo.tambah(new Produk(0, "Mouse Wireless", "elektronik", 250_000));
repo.tambah(new Produk(0, "Buku SQL Praktis", "buku", 180_000));
repo.tambah(new Produk(0, "Pensil Mekanik", "alat-tulis", 75_000));
Produk keyboard = repo.tambah(new Produk(0, "Keyboard Compact", "elektronik", 450_000));
System.out.println("produk terdaftar : " + repo.semua().size());
System.out.println();
System.out.println("== Kueri bermakna domain ==");
System.out.println("cariByKategori(\"buku\") : " + ringkas(repo.cariByKategori("buku")));
System.out.println("cariHargaDiBawah(200000) : " + ringkas(repo.cariHargaDiBawah(200_000)));
System.out.println("cariKataKunci(\"java\") : " + ringkas(repo.cariKataKunci("java")));
System.out.println();
System.out.println("== Semantik koleksi ==");
System.out.println("hapus(keyboard) : " + repo.hapus(keyboard)
+ " -> sisa: " + repo.semua().size());
System.out.println("cariById(5) : " + repo.cariById(5));
}
static List<String> ringkas(List<Produk> daftar) {
List<String> hasil = new ArrayList<>();
for (Produk p : daftar) {
hasil.add("#" + p.id() + " " + p.nama() + " (" + p.harga() + ")");
}
return hasil;
}
}
/** Entitas domain: kaya makna bisnis, bukan cerminan kolom tabel. */
record Produk(long id, String nama, String kategori, long harga) {
}
/** Kontrak repository: bahasa domain, bukan bahasa tabel. */
interface ProdukRepository {
Produk tambah(Produk p);
Optional<Produk> cariById(long id);
List<Produk> semua();
List<Produk> cariByKategori(String kategori);
List<Produk> cariHargaDiBawah(long batas);
List<Produk> cariKataKunci(String kata);
boolean hapus(Produk p);
}
/** Implementasi in-memory: koleksi objek domain sebagai sumber data. */
class ProdukRepositoryMemori implements ProdukRepository {
private final Map<Long, Produk> data = new LinkedHashMap<>();
private final AtomicLong urut = new AtomicLong();
@Override
public Produk tambah(Produk p) {
long id = urut.incrementAndGet();
Produk tersimpan = new Produk(id, p.nama(), p.kategori(), p.harga());
data.put(id, tersimpan);
return tersimpan;
}
@Override
public Optional<Produk> cariById(long id) {
return Optional.ofNullable(data.get(id));
}
@Override
public List<Produk> semua() {
return new ArrayList<>(data.values());
}
@Override
public List<Produk> cariByKategori(String kategori) {
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.kategori().equals(kategori)) {
hasil.add(p);
}
}
return hasil;
}
@Override
public List<Produk> cariHargaDiBawah(long batas) {
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.harga() < batas) {
hasil.add(p);
}
}
return hasil;
}
@Override
public List<Produk> cariKataKunci(String kata) {
String k = kata.toLowerCase(Locale.ROOT);
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.nama().toLowerCase(Locale.ROOT).contains(k)) {
hasil.add(p);
}
}
return hasil;
}
@Override
public boolean hapus(Produk p) {
return data.remove(p.id(), p); // hanya bila entitas yang sama pula tersimpan
}
}
ProdukRepositoryMemori.java
Unduh berkasrecord Produk(long id, String nama, String kategori, int harga) {
}
interface ProdukRepository {
Produk tambah(Produk p);
List<Produk> semua();
Optional<Produk> cariById(long id);
List<Produk> cariByKategori(String kategori);
List<Produk> cariHargaDiBawah(int batas);
List<Produk> cariKataKunci(String kata);
boolean hapus(Produk p);
}
class ProdukRepositoryMemori implements ProdukRepository {
private final Map<Long, Produk> data = new LinkedHashMap<>();
private final AtomicLong urutId = new AtomicLong();
@Override
public Produk tambah(Produk p) {
long idBaru = urutId.incrementAndGet();
Produk tersimpan = new Produk(idBaru, p.nama(), p.kategori(), p.harga());
data.put(idBaru, tersimpan);
return tersimpan;
}
@Override
public List<Produk> semua() {
return List.copyOf(data.values());
}
@Override
public Optional<Produk> cariById(long id) {
return Optional.ofNullable(data.get(id));
}
@Override
public List<Produk> cariByKategori(String kategori) {
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.kategori().equals(kategori)) {
hasil.add(p);
}
}
return hasil;
}
@Override
public List<Produk> cariHargaDiBawah(int batas) {
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.harga() < batas) {
hasil.add(p);
}
}
return hasil;
}
@Override
public List<Produk> cariKataKunci(String kata) {
String kunci = kata.toLowerCase(Locale.ROOT);
List<Produk> hasil = new ArrayList<>();
for (Produk p : data.values()) {
if (p.nama().toLowerCase(Locale.ROOT).contains(kunci)) {
hasil.add(p);
}
}
return hasil;
}
@Override
public boolean hapus(Produk p) {
return data.remove(p.id(), p);
}
}