Blog adalah kasus klasik relasi data: satu kategori menaungi banyak artikel, dan satu artikel menampung banyak comments. Document database tidak memaksa semua entitas menjadi collection terpisah — justru di sinilah keputusan perancangan berperan. Pada bagian ini kita memilah data blog ke dalam tiga collections, menetapkan cara menyambungkannya, lalu mengisi data awal yang akan dipakai seluruh bagian berikutnya.

Pola Akses Situs Blog #

Halaman-halaman situs blog menuntut query dengan bentuk berbeda:

  • Halaman depan: daftar artikel berstatus terbit, terbaru lebih dulu, per halaman kecil.
  • Halaman kategori: artikel terbit milik satu kategori tertentu.
  • Halaman artikel: isi lengkap artikel plus sepuluh komentar terakhir.
  • Moderasi: seluruh komentar satu artikel, termasuk yang lama.
  • Alur penulisan: menyimpan draf, lalu menerbitkannya dengan waktu terbit.

Pola ini menghasilkan pembagian alami: artikel dan kategori menyimpan konten, comments menyimpan diskusi yang tumbuh cepat, dan penulis — yang selalu tampil bersama artikelnya — cukup menjadi object embedded.

Tiga Collections dan Relasinya #

Relasi antar collection memakai reference — field yang menyimpan _id document lain. Field artikel.kategori_id menunjuk ke kategori._id, sedangkan comments.artikel_id menunjuk kembali ke artikel._id. Dua relasi tersebut mengikuti pola chapter Relasi One-to-Many: sisi “satu” menyimpan kunci, sisi “banyak” membaca kunci tersebut lewat query atau $lookup.

Khusus comments, kita menerapkan dua lapis sekaligus (detail di Bagian 3): collection comments menyimpan seluruh komentar, sementara artikel menyimpan salinan 10 komentar terbaru di field komentar_terbaru — subset pattern yang menjaga halaman artikel tetap ringan.

Keputusan _id #

Dua strategi _id dipakai bersamaan di proyek ini:

Collection_idAlasan
kategoriString slug, misal "database"Natural key: pendek, terbaca, dan dipakai langsung sebagai nilai kategori_id
artikelObjectId buatan dengan pola tetapNilai _id ikut serta di kursor pagination Bagian 4 — nilai tetap membuat seluruh output proyek dapat direproduksi

Nilai ObjectId buatan dihasilkan helper idArtikel(n) yang menyusun 24 digit hex tetap plus nomor urut. Reader asli bisa menggantinya dengan insertOne tanpa _id dan membiarkan MongoDB membuat ObjectId otomatis — perilaku proyek tidak berubah, hanya nilai kursor yang ikut berbeda.

Skrip Data Awal #

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

var blog = db.getSiblingDB("blog")

// _id tetap agar seluruh contoh proyek dapat direproduksi
function idArtikel(n) {
  return ObjectId("6780000000000000000000" + n.toString().padStart(2, "0"))
}

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

function siapkanKategori() {
  const hasil = blog.kategori.insertMany([
    { _id: "teknologi", nama: "Teknologi", deskripsi: "Tren dan perkakas teknologi terkini." },
    { _id: "database", nama: "Database", deskripsi: "Konsep database SQL dan NoSQL." },
    { _id: "produktivitas", nama: "Produktivitas", deskripsi: "Kebiasaan dan alat kerja yang efektif." },
  ])
  print("kategori dimasukkan:", Object.keys(hasil.insertedIds).length)
}

function siapkanArtikel() {
  const penulisSiti = { nama: "Siti Rahayu", email: "[email protected]" }
  const penulisBudi = { nama: "Budi Santoso", email: "[email protected]" }
  const penulisDewi = { nama: "Dewi Lestari", email: "[email protected]" }

  const hasil = blog.artikel.insertMany([
    {
      _id: idArtikel(1), judul: "Pengenalan Document Database",
      slug: "pengenalan-document-database",
      konten: "Document database menyimpan data sebagai document BSON yang strukturnya lentur. Artikel ini membahas bentuk dasarnya.",
      kategori_id: "database", penulis: penulisSiti, tag: ["mongodb", "dasar"],
      status: "terbit", published_at: ISODate("2026-01-12T08:00:00Z"),
    },
    {
      _id: idArtikel(2), judul: "Membandingkan Skema SQL dan Document",
      slug: "sql-vs-document",
      konten: "Tabel SQL menuntut bentuk seragam, document membiarkan tiap data membawa strukturnya sendiri. Kami membandingkan keduanya.",
      kategori_id: "database", penulis: penulisBudi, tag: ["sql", "mongodb"],
      status: "terbit", published_at: ISODate("2026-01-26T08:00:00Z"),
    },
    {
      _id: idArtikel(3), judul: "Index Compound untuk Query Umum",
      slug: "index-compound-query",
      konten: "Urutan field pada index compound menentukan query mana yang dilayani. Artikel ini mengangkat kasus katalog produk.",
      kategori_id: "database", penulis: penulisDewi, tag: ["index", "performa"],
      status: "terbit", published_at: ISODate("2026-02-09T08:00:00Z"),
    },
    {
      _id: idArtikel(4), judul: "Dasar Aggregation Pipeline",
      slug: "dasar-aggregation-pipeline",
      konten: "Pipeline mengubah data lewat rangkaian stage. Kami bedah $match, $group, dan $sort lewat contoh laporan.",
      kategori_id: "database", penulis: penulisSiti, tag: ["aggregation"],
      status: "terbit", published_at: ISODate("2026-02-23T08:00:00Z"),
    },
    {
      _id: idArtikel(5), judul: "Polymorphic Pattern dalam Praktik",
      slug: "polymorphic-pattern-praktik",
      konten: "Satu collection bisa memuat bentuk document berbeda yang berbagi field kunci. Rangkaian contoh kasusnya sedang disiapkan.",
      kategori_id: "database", penulis: penulisBudi, tag: ["pola", "skema"],
      status: "draf", published_at: null,
    },
    {
      _id: idArtikel(6), judul: "Memilih Keyboard untuk Produktivitas",
      slug: "memilih-keyboard-produktivitas",
      konten: "Tata letak, jenis sakelar, dan ukuran memengaruhi kenyamanan mengetik. Panduan singkat memilih yang pas.",
      kategori_id: "produktivitas", penulis: penulisDewi, tag: ["perangkat", "tips"],
      status: "terbit", published_at: ISODate("2026-03-05T08:00:00Z"),
    },
    {
      _id: idArtikel(7), judul: "Rutinitas Pagi Programmer",
      slug: "rutinitas-pagi-programmer",
      konten: "Menulis catatan sebelum membuka pesan membuat hari terasa lebih terkendali. Rutinitas kecil dengan dampak besar.",
      kategori_id: "produktivitas", penulis: penulisSiti, tag: ["kebiasaan"],
      status: "terbit", published_at: ISODate("2026-03-19T08:00:00Z"),
    },
    {
      _id: idArtikel(8), judul: "Alat CLI untuk Kerja Ringkas",
      slug: "alat-cli-kerja-ringkas",
      konten: "Sederet alat baris perintah yang menghemat waktu sehari-hari. Daftar lengkap sedang diurut berdasarkan frekuensi pemakaian.",
      kategori_id: "produktivitas", penulis: penulisBudi, tag: ["cli", "tips"],
      status: "draf", published_at: null,
    },
    {
      _id: idArtikel(9), judul: "Apa itu Replica Set",
      slug: "apa-itu-replica-set",
      konten: "Replica set menjaga salinan data di beberapa anggota sehingga baca tetap lancar saat satu anggota berhenti.",
      kategori_id: "teknologi", penulis: penulisDewi, tag: ["mongodb", "replikasi"],
      status: "terbit", published_at: ISODate("2026-04-16T08:00:00Z"),
    },
    {
      _id: idArtikel(10), judul: "Change Streams untuk Notifikasi Real-time",
      slug: "change-streams-notifikasi",
      konten: "Change streams memancarkan perubahan data begitu terjadi. Cocok untuk notifikasi dan sinkronisasi antarlayanan.",
      kategori_id: "teknologi", penulis: penulisBudi, tag: ["mongodb", "events"],
      status: "terbit", published_at: ISODate("2026-04-16T08:00:00Z"),
    },
    {
      _id: idArtikel(11), judul: "Vector Search untuk Pencarian Semantik",
      slug: "vector-search-pencarian",
      konten: "Pencarian semantik mencocokkan makna, bukan sekadar kata. Kami coba index vector untuk kumpulan artikel.",
      kategori_id: "teknologi", penulis: penulisSiti, tag: ["ai", "pencarian"],
      status: "terbit", published_at: ISODate("2026-05-07T08:00:00Z"),
    },
  ])
  print("artikel dimasukkan:", Object.keys(hasil.insertedIds).length)
}

function buatIndexAwal() {
  blog.artikel.createIndex({ slug: 1 }, { unique: true })
  print("index unik slug dibuat")
}

function contohArtikel() {
  print("contoh artikel (slug apa-itu-replica-set):")
  printjson(blog.artikel.findOne({ slug: "apa-itu-replica-set" }, { konten: 0 }))
}

resetDatabase()
siapkanKategori()
siapkanArtikel()
buatIndexAwal()
contohArtikel()

Jalankan dari folder proyek:

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

Output yang Dihasilkan:

text
database direset: blog
kategori dimasukkan: 3
artikel dimasukkan: 11
index unik slug dibuat
contoh artikel (slug apa-itu-replica-set):
{
  _id: ObjectId('678000000000000000000009'),
  judul: 'Apa itu Replica Set',
  slug: 'apa-itu-replica-set',
  kategori_id: 'teknologi',
  penulis: {
    nama: 'Dewi Lestari',
    email: '[email protected]'
  },
  tag: [
    'mongodb',
    'replikasi'
  ],
  status: 'terbit',
  published_at: ISODate('2026-04-16T08:00:00.000Z')
}

Mari kita bedah keputusan-keputusan di balik skrip ini:

  1. kategori memakai _id eksplisit berupa slug string. Sebagai akibatnya, kategori_id: "teknologi" pada artikel langsung terbaca tanpa perlu join — dan field _id kategori sekaligus menjadi satu-satunya kunci yang kita butuhkan.
  2. penulis di-embed, bukan direferensikan. Nama dan email selalu tampil bersama judul artikel, sehingga menyalinnya ke setiap artikel lebih murah daripada satu $lookup setiap kali halaman dibuka.
  3. status dan published_at berpasangan: artikel draf bernilai "draf" dengan published_at: null, artikel terbit bernilai "terbit" dengan waktu terbit. Dua field inilah yang menjadi dasar seluruh query tampilan pada Bagian 2 dan 4.
  4. createIndex({ slug: 1 }, { unique: true }) dipasang sejak awal karena slug adalah alamat URL artikel — duplikat slug berarti dua artikel berebut satu alamat. Tolakan duplikatnya berupa kode 11000, sama seperti sku pada proyek inventaris.
  5. Dua artikel sengaja berbagi tanggal terbit (apa-itu-replica-set dan change-streams-notifikasi, keduanya 16 April). Ini persiapan terselubung: pada Bagian 4, pasangan ini akan membuktikan mengapa kursor pagination membutuhkan _id sebagai pemecah seri.

Apa Berikutnya #

Tiga collections sudah berdiri dengan 11 artikel dan 3 kategori. Bagian 2 menghidupkan alur penulis: menyimpan draf baru, menerbitkannya, lalu menampilkan daftar artikel per kategori dan daftar terbaru di halaman depan.