Collection dari Bagian 1 sudah berisi sebelas artikel, tetapi belum ada cara menumbuhkannya. Blog yang hidup membutuhkan alur penulisan: penulis menyimpan draf, menyuntingnya berkali-kali, lalu menerbitkannya pada waktu yang dipilih — sementara pembaca hanya melihat artikel yang sudah terbit. Pada bagian ini alur tersebut menjadi empat fungsi dalam satu skrip, semuanya bertumpu pada pasangan field status dan published_at yang disiapkan sejak seed.

Draf, Terbit, dan Dua Field Penentu #

Satu artikel berpindah tepat satu kali antara dua kondisi:

Kondisi artikelstatuspublished_atMuncul di daftar?
Baru ditulis"draf"nullTidak
Sudah diterbitkan"terbit"waktu terbitYa

Field published_at sengaja diisi null pada artikel draf, bukan dihilangkan, sehingga satu filter { status: "terbit" } cukup untuk seluruh query tampilan. Skrip penulisan kita juga mencatat dibuat saat draf disimpan — artikel seed dari Bagian 1 tidak memuat field tersebut karena memang tidak pernah melalui fase draf di skrip ini.

Empat Fungsi Skrip #

FungsiOperasi MongoDBPeran
tulisDraf()findOne() + insertOne()Simpan draf baru, tolak slug yang sudah dipakai
terbitkanArtikel()updateOne() dengan $setIsi status dan published_at dalam satu operasi
daftarPerKategori()find() + sort() + projection()Halaman kategori: artikel terbit, terbaru lebih dulu
artikelTerbaru()find() + sort() + limit()Halaman depan: beberapa artikel terbaru

Skrip Artikel #

js
// blog-artikel.js — alur tulis draf, terbitkan, dan tampilkan artikel
// Jalankan setelah blog-seed.js:
//   mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/blog-artikel.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 tulisDraf(judul, slug, konten, kategoriId, penulis, tag, dibuat) {
  const sudah = blog.artikel.findOne({ slug: slug }, { _id: 1 })
  if (sudah) {
    print("slug sudah dipakai:", slug)
    return
  }
  const nomor = blog.artikel.countDocuments() + 1
  blog.artikel.insertOne({
    _id: idArtikel(nomor),
    judul: judul, slug: slug, konten: konten,
    kategori_id: kategoriId, penulis: penulis, tag: tag,
    status: "draf", published_at: null, dibuat: dibuat,
  })
  print("draf tersimpan:", slug)
}

function terbitkanArtikel(slug, tanggal) {
  const hasil = blog.artikel.updateOne(
    { slug: slug, status: "draf" },
    { $set: { status: "terbit", published_at: tanggal } }
  )
  print("terbitkan:", slug, "| cocok:", hasil.matchedCount, "| berubah:", hasil.modifiedCount)
}

function daftarPerKategori(kategoriId) {
  const kategori = blog.kategori.findOne({ _id: kategoriId })
  print("== artikel kategori", kategori.nama, "==")
  blog.artikel
    .find({ kategori_id: kategoriId, status: "terbit" })
    .sort({ published_at: -1 })
    .projection({ _id: 0, judul: 1, published_at: 1 })
    .forEach(function (a) {
      print(" ", a.judul, "|", a.published_at.toISOString().slice(0, 10))
    })
}

function artikelTerbaru(jumlah) {
  print("==", jumlah, "artikel terbaru ==")
  blog.artikel
    .find({ status: "terbit" })
    .sort({ published_at: -1 })
    .limit(jumlah)
    .projection({ _id: 0, judul: 1, published_at: 1 })
    .forEach(function (a) {
      print(" ", a.judul, "|", a.published_at.toISOString().slice(0, 10))
    })
}

print("== demo penulisan artikel ==")
tulisDraf(
  "Menjaga Skema Tetap Rapi", "menjaga-skema-rapi",
  "Schema yang rapi berawal dari pola akses yang jelas. Kami rangkum tiga kebiasaan perancangan yang terbukti membantu.",
  "database", { nama: "Dewi Lestari", email: "[email protected]" },
  ["skema", "tips"], ISODate("2026-05-20T09:00:00Z")
)
terbitkanArtikel("menjaga-skema-rapi", ISODate("2026-05-21T08:00:00Z"))
daftarPerKategori("database")
artikelTerbaru(3)

Jalankan setelah blog-seed.js:

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

Output yang Dihasilkan:

text
== demo penulisan artikel ==
draf tersimpan: menjaga-skema-rapi
terbitkan: menjaga-skema-rapi | cocok: 1 | berubah: 1
== artikel kategori Database ==
  Menjaga Skema Tetap Rapi | 2026-05-21
  Dasar Aggregation Pipeline | 2026-02-23
  Index Compound untuk Query Umum | 2026-02-09
  Membandingkan Skema SQL dan Document | 2026-01-26
  Pengenalan Document Database | 2026-01-12
== 3 artikel terbaru ==
  Menjaga Skema Tetap Rapi | 2026-05-21
  Vector Search untuk Pencarian Semantik | 2026-05-07
  Change Streams untuk Notifikasi Real-time | 2026-04-16

Bedah Fungsi demi Fungsi #

  1. tulisDraf() memeriksa slug dengan findOne() sebelum insert. Penjaga ini memberi pesan yang jelas (slug sudah dipakai) ketimbang membiarkan index unik melempar E11000 — keduanya melindungi alamat URL artikel, bedanya lapisan: skrip di sisi aplikasi, index di sisi server. Nomor _id dihitung dari countDocuments() + 1 sehingga artikel baru menjadi nomor 12, lalu insertOne menyimpannya dengan status: "draf" dan published_at: null.
  2. terbitkanArtikel() memakai filter dua syarat: { slug: slug, status: "draf" }. Menerbitkan artikel yang sudah terbit menghasilkan cocok: 0 tanpa mengubah apa pun — tombol “terbitkan” yang ditekan dua kali, atau skrip yang dijalankan ulang, tidak pernah menggeser waktu terbit lama. Dua field berubah dalam satu updateOne, jadi status dan published_at tidak mungkin saling tertinggal.
  3. daftarPerKategori() membaca kategori lebih dulu lewat natural key "database" — tanpa $lookup, karena _id kategori memang berupa slug yang tersimpan di kategori_id. Query artikel menyaring dua field, mengurutkan published_at: -1, lalu memproyeksikan judul dan tanggal. Dua artikel draf kategori database (Polymorphic Pattern, Alat CLI) tidak muncul meskipun kategori_id-nya cocok.
  4. artikelTerbaru() menambahkan limit(3) pada bentuk yang sama — versi ringkas halaman depan. Saat artikel bertambah ratusan, daftar ini harus dipecah menjadi halaman-halaman; cara memecahnya yang benar adalah tugas Bagian 4.

Perhatikan pula apa yang belum kita miliki: sampai titik ini collection artikel hanya memiliki index _id_ dan slug_1. Query daftar memindai seluruh collection (COLLSCAN) lalu mengurutkan hasilnya di memori (SORT). Dengan dua belas artikel durasinya nyaris tak terasa; dengan ribuan, pola ini menjadi sumber keluhan lambat yang klasik.

Jelajah Lanjut #

Beberapa percobaan layak dicoba sendiri sebelum lanjut: terbitkan ulang artikel yang sudah terbit dan amati cocok: 0, atau simpan draf baru dengan slug yang sudah dipakai dan baca penolakannya. Lalu periksa komposisi collection setelah alur ini — berapa artikel tiap kategori yang sudah terbit, dan berapa yang masih draf:

js
use blog
db.artikel.aggregate([
  { $group: { _id: { kategori: "$kategori_id", status: "$status" }, jumlah: { $sum: 1 } } },
  { $sort: { _id: 1 } }
])

Output yang Dihasilkan:

text
switched to db blog
[
  { _id: { kategori: 'database', status: 'draf' }, jumlah: 1 },
  { _id: { kategori: 'database', status: 'terbit' }, jumlah: 5 },
  { _id: { kategori: 'produktivitas', status: 'draf' }, jumlah: 1 },
  { _id: { kategori: 'produktivitas', status: 'terbit' }, jumlah: 2 },
  { _id: { kategori: 'teknologi', status: 'terbit' }, jumlah: 3 }
]

Satu stage $group merangkum hasil alur bagian ini: kategori database memuat lima artikel terbit — empat dari seed ditambah satu yang baru saja kita terbitkan — dan satu draf; produktivitas menyisakan satu draf; teknologi lengkap terbit.

Apa Berikutnya #

Artikel kini mengalir dari draf ke daftar terbaru, dan setiap query tampilan menyaring status: "terbit". Percakapan pembaca menyusul pada Bagian 3: sepuluh komentar terbaru disematkan langsung di artikel (subset pattern), collection comments menyimpan riwayat lengkap, dan $lookup menyatukan keduanya.