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:
| Field | Isi | Catatan |
|---|---|---|
_id | Resume token — posisi event di aliran oplog | Opaque; kunci resume di Chapter Resume Tokens |
operationType | insert, update, replace, delete, drop, invalidate | drop/invalidate dibahas di Chapter Menangani Interruption |
clusterTime | Timestamp dari logical clock cluster | Waktu logis, monoton dan konsisten lintas node |
wallTime | ISODate jam dinding server saat operasi | Untuk log dan tampilan manusia |
ns | { db, coll } — namespace sumber | Pembeda asal event pada stream database/cluster |
documentKey | _id document yang berubah | Di sharded cluster memuat juga shard key |
fullDocument | Isi document utuh pasca operasi | insert/replace selalu; update bila diminta; delete tidak pernah |
updateDescription | Diff: updatedFields, removedFields, truncatedArrays | Hanya untuk operationType: "update" |
fullDocumentBeforeChange | Isi document sebelum operasi | Hanya bila pre-image diaktifkan lewat collMod |
Kita buktikan pola ketersediaan kedua field terpenting dengan empat operasi berurutan pada stream tanpa opsi:
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:
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: falseEmpat 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:
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:
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:
updatedFields— peta field ke nilai barunya:total: 205000dankurir: "JNE"dari$set. Field yang tidak disentuh tidak muncul, sebesar apa pun document-nya.removedFields— daftar nama field yang dihapus lewat$unset:dikirim.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.
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:
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:
db.runCommand({ collMod: "pesanan", changeStreamPreAndPostImages: { enabled: false } }).okOutput yang Dihasilkan:
1Rangkuman #
- Field event mengikuti
operationType:insert/replacemembawafullDocument,updatemembawaupdateDescription(diff),deletehanyadocumentKey. updateDescription.updatedFields/removedFieldsadalah 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.fullDocumentBeforeChangemenghadirkan kondisi sebelum update/delete untuk audit, dengan biaya tulisan tambahan kesystem.preimages— aktifkan per collection saja.clusterTimeuntuk mesin (urutan, deduplikasi),wallTimeuntuk manusia.