Setiap change event yang keluar dari watch() adalah satu document BSON terstruktur — kontrak antara server dan konsumen. Kontrak itu tidak seragam: fullDocument selalu hadir untuk insert, tidak pernah untuk delete, dan hanya muncul untuk update bila kita memintanya. Konsumen yang menganggap semua field selalu terisi akan menulis kode yang gagal diam-diam. Chapter ini membedah field demi field dengan event nyata, membedah diff updateDescription, lalu mengaktifkan pre-image agar event update dan delete bisa membawa kondisi document sebelum perubahan.

Peta Field Event #

Satu event bisa memuat sembilan field. Tabel berikut adalah peta lengkapnya:

FieldIsiCatatan
_idResume token — posisi event di aliran oplogOpaque; kunci resume di Chapter Resume Tokens
operationTypeinsert, update, replace, delete, drop, invalidatedrop/invalidate dibahas di Chapter Menangani Interruption
clusterTimeTimestamp dari logical clock clusterWaktu logis, monoton dan konsisten lintas node
wallTimeISODate jam dinding server saat operasiUntuk log dan tampilan manusia
ns{ db, coll } — namespace sumberPembeda asal event pada stream database/cluster
documentKey_id document yang berubahDi sharded cluster memuat juga shard key
fullDocumentIsi document utuh pasca operasiinsert/replace selalu; update bila diminta; delete tidak pernah
updateDescriptionDiff: updatedFields, removedFields, truncatedArraysHanya untuk operationType: "update"
fullDocumentBeforeChangeIsi document sebelum operasiHanya bila pre-image diaktifkan lewat collMod

Kita buktikan pola ketersediaan kedua field terpenting dengan empat operasi berurutan pada stream tanpa opsi:

js
use latihan_events
db.dropDatabase()
db.pesanan.insertOne({ kode: "PSN-001", pelanggan: "Budi", total: 250000, status: "baru" })
const cs = db.pesanan.watch()
cs.disableBlockWarnings()
db.pesanan.insertOne({ kode: "PSN-002", pelanggan: "Siti", total: 175000, status: "baru" })
db.pesanan.updateOne({ kode: "PSN-002" }, { $set: { total: 190000 }, $unset: { status: "" } })
db.pesanan.replaceOne({ kode: "PSN-002" }, { kode: "PSN-002", pelanggan: "Siti", total: 190000, dikirim: true })
db.pesanan.deleteOne({ kode: "PSN-001" })
for (let i = 1; i <= 4; i++) {
  const ev = cs.next()
  print(ev.operationType + " -> fullDocument: " + ("fullDocument" in ev) + ", updateDescription: " + ("updateDescription" in ev))
}
cs.close()

Output yang Dihasilkan:

text
switched to db latihan_events
{ ok: 1, dropped: 'latihan_events' }
{
  acknowledged: true,
  insertedId: ObjectId('6ad3e9d01faabbb031888d23')
}

ChangeStreamCursor on pesanan
{
  acknowledged: true,
  insertedId: ObjectId('6ad3e9d11faabbb031888d24')
}
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{ acknowledged: true, deletedCount: 1 }
insert -> fullDocument: true, updateDescription: false
update -> fullDocument: false, updateDescription: true
replace -> fullDocument: true, updateDescription: false
delete -> fullDocument: false, updateDescription: false

Empat baris terakhir merangkum seluruh kontrak: insert dan replace membawa fullDocument tetapi bukan diff; update membawa diff tetapi bukan fullDocument; delete tidak membawa keduanya karena document-nya sudah tidak ada — hanya documentKey yang tersisa sebagai pegangan. Filter operationType di pipeline watch() (Chapter Menonton Satu Collection) bekerja langsung pada field kedua tabel ini.

Diff Update di updateDescription #

Untuk update, informasi terkaya justru bukan isi document, melainkan apa yang berubah. Oplog menyimpan diff operasi, dan updateDescription meneruskannya apa adanya. Kita tampilkan satu event update utuh dari operasi yang mengubah dua field sekaligus dan menghapus satu field:

js
const csU = db.pesanan.watch()
csU.disableBlockWarnings()
db.pesanan.updateOne({ kode: "PSN-002" }, { $set: { total: 205000, kurir: "JNE" }, $unset: { dikirim: "" } })
printjson(csU.next())
csU.close()

Output yang Dihasilkan:

text
ChangeStreamCursor on pesanan
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{
  _id: {
    _data: '826AD3E9D2000000012B042C0100296E5A10048895FC08307B4FA9874EDA77DE6BC593463C6F7065726174696F6E54797065003C7570646174650046646F63756D656E744B65790046645F696400646AD3E9D11FAABBB031888D24000004'
  },
  operationType: 'update',
  clusterTime: Timestamp({ t: 1792272850, i: 1 }),
  wallTime: ISODate('2026-10-17T21:34:10.945Z'),
  ns: {
    db: 'latihan_events',
    coll: 'pesanan'
  },
  documentKey: {
    _id: ObjectId('6ad3e9d11faabbb031888d24')
  },
  updateDescription: {
    updatedFields: {
      total: 205000,
      kurir: 'JNE'
    },
    removedFields: [
      'dikirim'
    ],
    truncatedArrays: []
  }
}

Tiga bagian updateDescription masing-masing punya peran:

  1. updatedFields — peta field ke nilai barunya: total: 205000 dan kurir: "JNE" dari $set. Field yang tidak disentuh tidak muncul, sebesar apa pun document-nya.
  2. removedFields — daftar nama field yang dihapus lewat $unset: dikirim.
  3. truncatedArrays — array yang dipangkas server karena terlalu besar untuk dimuat utuh di event; setiap entrinya memuat nama field dan ukuran baru array. Kosong pada operasi kita.

Sisa field mengikuti tabel: documentKey menunjuk _id PSN-002, ns menunjuk latihan_events.pesanan, dan clusterTime/wallTime mencatat versi logical clock (bertipe Timestamp) serta jam dinding (bertipe ISODate). Gunakan clusterTime untuk pengurutan dan deduplikasi, wallTime untuk log yang dibaca manusia.

fullDocumentBeforeChange lewat Pre-Image #

Dua pertanyaan sering muncul saat membangun audit trail: “bagaimana isi document sebelum dihapus?” dan “bagaimana hasil persis sebuah update?”. Jawaban default MongoDB: tidak tersedia — oplog hanya menyimpan diff. Sejak MongoDB 6.0 ada opsinya: aktifkan pre- and post-images per collection dengan collMod, lalu minta lewat opsi stream.

js
db.runCommand({ collMod: "pesanan", changeStreamPreAndPostImages: { enabled: true } }).ok
db.getCollectionInfos({ name: "pesanan" })[0].options
const csP = db.pesanan.watch([], { fullDocument: "whenAvailable", fullDocumentBeforeChange: "whenAvailable" })
csP.disableBlockWarnings()
db.pesanan.updateOne({ kode: "PSN-002" }, { $set: { total: 220000 } })
db.pesanan.deleteOne({ kode: "PSN-002" })
const evU = csP.next()
const evD = csP.next()
({ jenis: [evU.operationType, evD.operationType], sebelum: evU.fullDocumentBeforeChange, diff: evU.updateDescription, sesudah: evU.fullDocument, totalSebelumDelete: evD.fullDocumentBeforeChange.total })
csP.close()

Output yang Dihasilkan:

text
1
{ changeStreamPreAndPostImages: { enabled: true } }

ChangeStreamCursor on pesanan
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{ acknowledged: true, deletedCount: 1 }

{
  jenis: [ 'update', 'delete' ],
  sebelum: {
    _id: ObjectId('6ad3e9d11faabbb031888d24'),
    kode: 'PSN-002',
    pelanggan: 'Siti',
    total: 205000,
    kurir: 'JNE'
  },
  diff: {
    updatedFields: { total: 220000 },
    removedFields: [],
    truncatedArrays: []
  },
  sesudah: {
    _id: ObjectId('6ad3e9d11faabbb031888d24'),
    kode: 'PSN-002',
    pelanggan: 'Siti',
    total: 220000,
    kurir: 'JNE'
  },
  totalSebelumDelete: 220000
}

Sekarang satu event update membawa tiga pandangan sekaligus: sebelum (pre-image), diff (updateDescription), dan sesudah (post-image). Event delete pun ikut membawa totalSebelumDelete — salinan terakhir document sebelum dihapus. Bedakan dua mode fullDocument untuk update:

  • "updateLookup" (Chapter Menonton Satu Collection) — server membaca ulang document saat event diproses; bila update kedua menyusul sangat cepat, hasilnya bisa sudah memuat perubahan kedua.
  • "whenAvailable" — post-image snapshot yang dibuat bersamaan dengan operasinya; tepat merepresentasikan hasil operasi, bukan kondisi terbaru.

Pre-image bukan gratis. MongoDB menyimpan salinan sebelum/sesudah di collection internal system.preimages, sehingga setiap update dan delete pada collection itu menimbulkan tulisan tambahan; salinan ikut terhapus saat entri oplog terkait terdorong keluar jendela (atau lebih cepat lewat expireAfterSeconds). Karena itu fitur ini nonaktif secara default — aktifkan hanya pada collection yang benar-benar membutuhkannya, lalu matikan kembali begitu tidak dipakai:

js
db.runCommand({ collMod: "pesanan", changeStreamPreAndPostImages: { enabled: false } }).ok

Output yang Dihasilkan:

text
1

Rangkuman #

  • Field event mengikuti operationType: insert/replace membawa fullDocument, update membawa updateDescription (diff), delete hanya documentKey.
  • updateDescription.updatedFields/removedFields adalah diff langsung dari oplog — murah, tetapi bukan isi document.
  • fullDocument: "updateLookup" membaca ulang document saat event diproses; fullDocument: "whenAvailable" memberi post-image snapshot persis, dengan syarat pre/post-image diaktifkan.
  • fullDocumentBeforeChange menghadirkan kondisi sebelum update/delete untuk audit, dengan biaya tulisan tambahan ke system.preimages — aktifkan per collection saja.
  • clusterTime untuk mesin (urutan, deduplikasi), wallTime untuk manusia.