Saat aplikasi tumbuh, panggilan find() dan insertOne() perlahan menyebar ke semua route handler. Niat bisnis tenggelam di balik detail query: db.produk.find({ stok: { $gt: 0 } }).sort(...) tidak mengatakan bahwa sebenarnya kita sedang mengambil produk unggulan. Setiap pengujian memaksa seluruh database ikut berjalan. Repository pattern memotong masalah ini dengan satu lapisan tipis: akses data dikumpulkan di objek yang metodenya berbahasa domain — dan karena metodenya hanya fungsi biasa, pengujian bisa memakai pengganti tanpa database sama sekali. Chapter ini membangun lapisan itu di mongosh, lalu mengujinya dengan repository sungguhan dan repository tiruan.

Lapisan Aplikasi yang Berlapis #

Pola ini menempatkan repository sebagai satu-satunya jalur keluar-masuk data:

Ketiga lapisan punya tanggung jawab yang tidak saling menimpa. Route menerjemahkan HTTP menjadi panggilan service; service menjalankan aturan bisnis tanpa tahu bentuk penyimpanan; repository menerjemahkan pertanyaan domain menjadi operasi driver. Manfaatnya konkret: query hanya ditulis satu kali, perubahan schema menyentuh satu file, dan service bisa diuji dengan repository palsu yang cepat dan deterministik.

Kita bangun repository untuk toko buku kita — lengkap dengan tiga metode domain:

js
use latihan_repository
db.dropDatabase()
db.produk.insertMany([
  { _id: "PRD-001", nama: "Kopi Gayo", harga: 85000, stok: 12, terjual: 340 },
  { _id: "PRD-002", nama: "Teh Melati", harga: 32000, stok: 0, terjual: 275 },
  { _id: "PRD-003", nama: "Gula Aren", harga: 15000, stok: 40, terjual: 190 }
])

const buatRepositoryToko = (db) => ({
  ambilProdukUnggulan(batas) {
    return db.produk.find({ stok: { $gt: 0 } }, { nama: 1, terjual: 1, _id: 0 }).sort({ terjual: -1 }).limit(batas).toArray()
  },
  buatPesanan(item) {
    const hasil = db.pesanan.insertOne({ ...item, status: "baru", dibuat: new Date() })
    return { id: hasil.insertedId, kode: item.kode }
  },
  hitungPesananPelanggan(pelanggan) {
    return db.pesanan.countDocuments({ pelanggan: pelanggan })
  }
})

const repo = buatRepositoryToko(db)
repo.ambilProdukUnggulan(2)

Output yang Dihasilkan:

text
switched to db latihan_repository
{ ok: 1, dropped: 'latihan_repository' }
{
  acknowledged: true,
  insertedIds: { '0': 'PRD-001', '1': 'PRD-002', '2': 'PRD-003' }
}

[
  { nama: 'Kopi Gayo', terjual: 340 },
  { nama: 'Gula Aren', terjual: 190 }
]

Bedah baris per baris:

  1. buatRepositoryToko(db) — factory yang menerima objek db; koneksi tidak dibuat di dalam repository, melainkan disuntikkan dari luar. Inilah dependency injection dalam bentuk paling sederhana, dan alasan repository mudah diuji.
  2. ambilProdukUnggulan(batas) — definisi “unggulan” (stok tersisa, diurutkan penjualan) hidup di satu tempat. Pemanggil hanya bertanya; bila besok kriteria berubah (misalnya minimal rating), hanya method ini yang diubah.
  3. buatPesanan(item) — field operasional (status: "baru", dibuat) diisi repository, bukan route; pemanggil tidak perlu tahu field apa yang wajib.
  4. hitungPesananPelanggan(pelanggan) — pembungkus countDocuments dengan nama yang bisa dibaca reviewer non-teknis.

Pemakaiannya kini berbunyi seperti proses bisnis, bukan seperti query:

js
repo.buatPesanan({ kode: "PSN-001", pelanggan: "Budi", total: 117000 })
repo.buatPesanan({ kode: "PSN-002", pelanggan: "Budi", total: 85000 })
repo.hitungPesananPelanggan("Budi")

Output yang Dihasilkan:

text
{ id: ObjectId('6ad3ebe5e1c7c8969fb8d0c9'), kode: 'PSN-001' }
{ id: ObjectId('6ad3ebe5e1c7c8969fb8d0ca'), kode: 'PSN-002' }
2

Service dan Pengujian dengan Mock #

Kekuatan pola ini baru terasa saat service mengonsumsi repository lewat bentuknya, bukan implementasinya. Fungsi berikut menerima repository apa pun yang menyediakan dua metode:

js
const laporanPelanggan = (repo, nama) => ({
  pelanggan: nama,
  jumlahPesanan: repo.hitungPesananPelanggan(nama),
  produkTerlaris: repo.ambilProdukUnggulan(1)[0].nama
})

// pengujian integrasi: repository sungguhan + MongoDB hidup
laporanPelanggan(repo, "Budi")

// pengujian unit: mock tanpa database sama sekali
const repoMock = {
  hitungPesananPelanggan: () => 7,
  ambilProdukUnggulan: (batas) => [{ nama: "Lada Hitam" }]
}
laporanPelanggan(repoMock, "Budi")

Output yang Dihasilkan:

text
{ pelanggan: 'Budi', jumlahPesanan: 2, produkTerlaris: 'Kopi Gayo' }

{ pelanggan: 'Budi', jumlahPesanan: 7, produkTerlaris: 'Lada Hitam' }

Satu fungsi, dua hasil: repository sungguhan membaca MongoDB (dua pesanan Budi, produk terlaris Kopi Gayo), sedangkan mock membalas data statis dalam mikrodetik. Dalam rangka pengujian, mock dipakai untuk kasus yang menyulitkan database asli — pelanggan dengan ribuan pesanan, kondisi error, nilai batas — tanpa menyiapkan data seed. Repository sungguhan tetap diuji terpisah dengan sedikit skenario integrasi terhadap mongod lokal. Di TypeScript atau Java, “bentuk” repository ini dinyatakan eksplisit sebagai interface; di JavaScript dinamis, kesesuaian metode cukup dijaga konvensi dan pengujian.

Batas Abstraksi #

Repository yang terlalu rajin menjadi beban sendiri. Empat batas yang layak dipegang:

  • Jangan membungkus driver satu-ke-satu. repoFind(filter) yang hanya meneruskan ke find() menambah lapisan tanpa makna; setiap method harus punya nama dan aturan domain.
  • Aggregation tetap di dalam, dengan nama bisnis. Pipeline ringkasPenjualanHarian() boleh panjang — yang penting pemanggil melihat method bernama jelas, bukan pipeline mentah.
  • Satu repository per agregat (produk, pesanan, pelanggan), bukan satu repository raksasa untuk seluruh database, dan bukan pula satu per collection mentah.
  • Pilih kebijakan ObjectId dan patuhi. Repository bisa memetakan _id ke string di batas lapisan, atau membiarkan ObjectId mengalir — yang buruk adalah campuran keduanya yang memaksa pemanggil menebak.

Sisa dari section ini sudah menyiapkan fondasinya: lifecycle koneksi (Chapter Connection Lifecycle), pooling (Chapter Connection Pooling), dan penanganan error (Chapter Error Handling) adalah urusan driver yang tetap berlaku apa pun lapisannya. Implementasi repository sungguhan dengan driver resmi — lengkap dengan interface TypeScript dan class PyMongo — kita tulis di Section MongoDB dengan JavaScript dan MongoDB dengan Python.

Rangkuman #

  • Repository mengumpulkan akses data di balik metode berbahasa domain; route dan service berhenti menulis query mentah.
  • Factory + dependency injection (buatRepositoryToko(db)) membuat repository bisa ditukar; koneksi dikelola di luar.
  • Service mengonsumsi bentuk repository, sehingga bisa diuji unit dengan mock cepat dan diuji integrasi dengan mongod lokal.
  • Batas abstraksi: method harus punya makna domain, aggregation disembunyikan di balik nama, satu repository per agregat, kebijakan ObjectId konsisten.