Setiap schema MongoDB yang baik berawal dari satu pertanyaan: query seperti apa yang akan sering dijalankan aplikasi? Berbeda dengan pendekatan normalisasi tabel SQL, document database justru menyatukan data yang selalu dibaca bersamaan. Pada bagian ini kita membedah kebutuhan gudang toko perangkat komputer, mengambil keputusan embed atau batasi untuk setiap potongan data, lalu menuangkan hasilnya menjadi skrip data awal yang dapat dijalankan ulang.

Pola Akses Aplikasi #

Sebelum menulis satu document pun, kita daftarkan dulu pertanyaan yang paling sering diajukan kepada data:

  • Daftar produk dalam satu kategori, disaring rentang harga, diurutkan dari termurah.
  • Detail satu produk berdasarkan sku — lengkap dengan informasi pemasok untuk pemesanan ulang.
  • Pergerakan stok satu produk (kapan masuk, kapan keluar, berapa jumlahnya).
  • Rekap bulanan seluruh pergerakan stok dan total nilai persediaan per kategori.
  • Pencarian bebas berdasarkan nama produk.

Pola ini menunjukkan bahwa produk hampir selalu dibaca sebagai satu kesatuan: katalog, harga, stok, pemasok, dan riwayatnya. Data tersebut layak tinggal dalam satu collection produk.

Keputusan Perancangan #

Empat keputusan utama membentuk schema ini:

AspekKeputusanAlasan
PemasokEmbedded object di dalam produkSelalu dibaca bersama produk, jarang berubah, tidak diquery mandiri
KategoriField string biasaRendah kardinalitas, cukup untuk filter dan pengelompokan laporan
Riwayat stokArray embedded, dibatasi 20 entriDibaca per produk, tetapi harus dibatasi agar document tidak tumbuh tanpa kendali
Transaksi masuk/keluarDicatat sebagai entri riwayat, bukan collection terpisahPola aksesnya per produk; collection terpisah hanya perlu bila transaksi diquery lintas produk secara mandiri

Khusus untuk kategori, pendekatan string sengaja dipilih daripada collection referensi. Empat nilai yang stabil (keyboard, monitor, mouse, aksesori) tidak membutuhkan relasi — memakai reference justru menambah satu $lookup hanya untuk menampilkan label. Keputusan ini boleh ditinjau ulang jika suatu saat kategori perlu menyimpan deskripsi panjang atau dihitung jumlah produknya secara rutin.

Struktur Document Produk #

Bentuk akhir setiap document produk:

text
{
  sku: "KB-001",              // identitas unik produk
  nama: "Keyboard Mekanik 60%",
  kategori: "keyboard",
  harga: 750000,
  stok: 12,                   // nilai otoritatif terkini
  aktif: true,                // produk masih dijual atau tidak
  pemasok: { nama, email, kota },          // embedded
  riwayat_stok: [ { tanggal, jenis, jumlah } ]   // array terbatas
}

Dua field terakhir memegang peran penting. Field stok adalah angka otoritatif yang selalu mutakhir — aplikasi tidak perlu menjumlahkan riwayat hanya untuk mengetahui stok saat ini. Array riwayat_stok berperan sebagai jejak audit per produk; konsistensinya dijaga dengan aturan sederhana: setiap perubahan stok selalu disertai satu entri riwayat baru.

Skrip Data Awal #

Skrip berikut mereset database lalu mengisi sembilan produk dengan riwayat yang konsisten. Tanggal dibuat tetap (Januari–April 2026) agar setiap output proyek dapat dibandingkan:

js
// inventaris-seed.js — data awal proyek manajemen inventaris
// Jalankan dari folder proyek:
//   mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/inventaris-seed.js

var inv = db.getSiblingDB("inventaris")

function resetDatabase() {
  inv.dropDatabase()
  print("database direset: inventaris")
}

function isiProduk() {
  const pemasokSinar = { nama: "PT Sinar Teknik", email: "[email protected]", kota: "Bandung" }
  const pemasokDigitral = { nama: "CV Digitral Perkasa", email: "[email protected]", kota: "Surabaya" }
  const pemasokAneka = { nama: "PT Aneka Peripheral", email: "[email protected]", kota: "Jakarta" }

  const hasil = inv.produk.insertMany([
    {
      sku: "KB-001", nama: "Keyboard Mekanik 60%", kategori: "keyboard",
      harga: 750000, stok: 12, aktif: true, pemasok: pemasokSinar,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-05T00:00:00Z"), jenis: "masuk", jumlah: 15 },
        { tanggal: ISODate("2026-02-10T00:00:00Z"), jenis: "keluar", jumlah: 3 },
      ],
    },
    {
      sku: "KB-002", nama: "Keyboard Membrane Standar", kategori: "keyboard",
      harga: 150000, stok: 45, aktif: true, pemasok: pemasokDigitral,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-05T00:00:00Z"), jenis: "masuk", jumlah: 60 },
        { tanggal: ISODate("2026-03-02T00:00:00Z"), jenis: "keluar", jumlah: 15 },
      ],
    },
    {
      sku: "KB-003", nama: "Keyboard Split Ergonomis", kategori: "keyboard",
      harga: 1200000, stok: 3, aktif: true, pemasok: pemasokSinar,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-06T00:00:00Z"), jenis: "masuk", jumlah: 5 },
        { tanggal: ISODate("2026-02-18T00:00:00Z"), jenis: "keluar", jumlah: 2 },
      ],
    },
    {
      sku: "MN-001", nama: "Monitor IPS 24 inci", kategori: "monitor",
      harga: 1850000, stok: 8, aktif: true, pemasok: pemasokAneka,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-07T00:00:00Z"), jenis: "masuk", jumlah: 10 },
        { tanggal: ISODate("2026-03-11T00:00:00Z"), jenis: "keluar", jumlah: 2 },
      ],
    },
    {
      sku: "MN-002", nama: "Monitor VA 27 inci", kategori: "monitor",
      harga: 2400000, stok: 15, aktif: true, pemasok: pemasokAneka,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-07T00:00:00Z"), jenis: "masuk", jumlah: 20 },
        { tanggal: ISODate("2026-04-01T00:00:00Z"), jenis: "keluar", jumlah: 5 },
      ],
    },
    {
      sku: "MS-001", nama: "Mouse Wireless Ringan", kategori: "mouse",
      harga: 275000, stok: 30, aktif: true, pemasok: pemasokDigitral,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-08T00:00:00Z"), jenis: "masuk", jumlah: 40 },
        { tanggal: ISODate("2026-02-20T00:00:00Z"), jenis: "keluar", jumlah: 10 },
      ],
    },
    {
      sku: "MS-002", nama: "Mouse Gaming 8 Tombol", kategori: "mouse",
      harga: 680000, stok: 6, aktif: true, pemasok: pemasokSinar,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-08T00:00:00Z"), jenis: "masuk", jumlah: 10 },
        { tanggal: ISODate("2026-03-15T00:00:00Z"), jenis: "keluar", jumlah: 4 },
      ],
    },
    {
      sku: "AK-001", nama: "Mousepad Kain XL", kategori: "aksesori",
      harga: 95000, stok: 60, aktif: true, pemasok: pemasokDigitral,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-09T00:00:00Z"), jenis: "masuk", jumlah: 80 },
        { tanggal: ISODate("2026-04-05T00:00:00Z"), jenis: "keluar", jumlah: 20 },
      ],
    },
    {
      sku: "AK-002", nama: "Dock USB-C 6-in-1", kategori: "aksesori",
      harga: 520000, stok: 9, aktif: true, pemasok: pemasokAneka,
      riwayat_stok: [
        { tanggal: ISODate("2026-01-09T00:00:00Z"), jenis: "masuk", jumlah: 12 },
        { tanggal: ISODate("2026-02-25T00:00:00Z"), jenis: "keluar", jumlah: 3 },
      ],
    },
  ])

  print("produk dimasukkan:", Object.keys(hasil.insertedIds).length)
  print("kategori terdaftar:", inv.produk.distinct("kategori"))
  print("contoh document (KB-001):")
  printjson(inv.produk.findOne({ sku: "KB-001" }, { _id: 0 }))
}

resetDatabase()
isiProduk()

Jalankan dari folder proyek:

bash
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/inventaris-seed.js

Output yang Dihasilkan:

text
database direset: inventaris
produk dimasukkan: 9
kategori terdaftar: [ 'aksesori', 'keyboard', 'monitor', 'mouse' ]
contoh document (KB-001):
{
  sku: 'KB-001',
  nama: 'Keyboard Mekanik 60%',
  kategori: 'keyboard',
  harga: 750000,
  stok: 12,
  aktif: true,
  pemasok: {
    nama: 'PT Sinar Teknik',
    email: '[email protected]',
    kota: 'Bandung'
  },
  riwayat_stok: [
    {
      tanggal: ISODate('2026-01-05T00:00:00.000Z'),
      jenis: 'masuk',
      jumlah: 15
    },
    {
      tanggal: ISODate('2026-02-10T00:00:00.000Z'),
      jenis: 'keluar',
      jumlah: 3
    }
  ]
}

Mari kita bedah bagian-bagian kuncinya:

  1. var inv = db.getSiblingDB("inventaris") memilih database tanpa perintah use yang hanya berlaku di REPL. Semua skrip proyek memakai pola yang sama sehingga bisa dijalankan lewat --file.
  2. resetDatabase() menjalankan dropDatabase() lebih dulu. Skrip seed selalu bisa diulang dari kondisi bersih — kebiasaan yang membuat seluruh proyek dapat direproduksi.
  3. const pemasokSinar = {...} lalu dipakai berulang kali di insertMany. MongoDB menyalin object tersebut ke setiap document — persis perilaku embedding yang kita inginkan, tanpa referensi tersembunyi antar document.
  4. Setiap produk menyimpan riwayat awal yang jumlahnya cocok dengan stok — misalnya KB-001 masuk 15 lalu keluar 3, tersisa 12. Konsistensi ini penting agar rekap bulanan pada Bagian 4 dapat dipercaya.
  5. Object.keys(hasil.insertedIds).length menghitung banyaknya document yang benar-benar tersimpan dari hasil insertMany — di mongosh, object hasil tersebut memuat insertedIds berbentuk peta indeks.

Apa Berikutnya #

Schema telah berdiri beserta data awalnya. Bagian 2 menghidupkan gudang ini dengan empat fungsi inti: menambah produk baru, mencatat transaksi stok secara atomik, menonaktifkan produk lama, dan menghapus dengan pengaman dua langkah.