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 artikel | status | published_at | Muncul di daftar? |
|---|---|---|---|
| Baru ditulis | "draf" | null | Tidak |
| Sudah diterbitkan | "terbit" | waktu terbit | Ya |
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 #
| Fungsi | Operasi MongoDB | Peran |
|---|---|---|
tulisDraf() | findOne() + insertOne() | Simpan draf baru, tolak slug yang sudah dipakai |
terbitkanArtikel() | updateOne() dengan $set | Isi 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 #
// 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:
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/blog-artikel.jsOutput yang Dihasilkan:
== 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-16Bedah Fungsi demi Fungsi #
tulisDraf()memeriksa slug denganfindOne()sebelum insert. Penjaga ini memberi pesan yang jelas (slug sudah dipakai) ketimbang membiarkan index unik melemparE11000— keduanya melindungi alamat URL artikel, bedanya lapisan: skrip di sisi aplikasi, index di sisi server. Nomor_iddihitung daricountDocuments() + 1sehingga artikel baru menjadi nomor 12, laluinsertOnemenyimpannya denganstatus: "draf"danpublished_at: null.terbitkanArtikel()memakai filter dua syarat:{ slug: slug, status: "draf" }. Menerbitkan artikel yang sudah terbit menghasilkancocok: 0tanpa mengubah apa pun — tombol “terbitkan” yang ditekan dua kali, atau skrip yang dijalankan ulang, tidak pernah menggeser waktu terbit lama. Dua field berubah dalam satuupdateOne, jadistatusdanpublished_attidak mungkin saling tertinggal.daftarPerKategori()membaca kategori lebih dulu lewat natural key"database"— tanpa$lookup, karena_idkategori memang berupa slug yang tersimpan dikategori_id. Query artikel menyaring dua field, mengurutkanpublished_at: -1, lalu memproyeksikan judul dan tanggal. Dua artikel draf kategori database (Polymorphic Pattern, Alat CLI) tidak muncul meskipunkategori_id-nya cocok.artikelTerbaru()menambahkanlimit(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:
use blog
db.artikel.aggregate([
{ $group: { _id: { kategori: "$kategori_id", status: "$status" }, jumlah: { $sum: 1 } } },
{ $sort: { _id: 1 } }
])Output yang Dihasilkan:
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.