MongoDB tidak memaksakan satu bentuk tabel seperti basis data relasional, sehingga setiap rancangan diawali dengan keputusan tentang letak data yang saling terkait. Strategi pertama adalah embedding, yaitu menanam subdokumen atau array di dalam dokumen induk. Strategi kedua adalah referencing, yaitu menyimpan ObjectId milik dokumen lain sebagai penghubung antarkoleksi. Topik ini membahas cara kerja keduanya pada basis data kampus, biaya baca yang menyertainya, serta kriteria menimbang yang melibatkan pola akses, ukuran data, dan pertumbuhan.
Embedding menanam data terkait di dalam dokumen induk#
Embedding menyimpan data anak sebagai subdokumen atau array di dalam dokumen induknya, sehingga satu pembacaan dokumen mengembalikan seluruh data yang dibutuhkan. Bentuk ini dipilih apabila data anak hanya bermakna bersama induknya, jarang dibaca secara terpisah, dan tidak bertumbuh tanpa batas. Contoh pada domain kampus adalah alamat mahasiswa, yang hampir selalu ditampilkan bersama profil mahasiswa itu sendiri.
use kampusswitched to db kampusdb.mahasiswa.insertOne({
nama: "Budi Santoso",
nim: "10112001",
alamat: {
jalan: "Jl. Kenanga No. 7",
kota: "Bandung",
kodePos: "40135"
}
}){
acknowledged: true,
insertedId: ObjectId('6ae8992ef71ce5016d8de666')
}db.mahasiswa.findOne({ nim: "10112001" }){
_id: ObjectId('6ae8992ef71ce5016d8de666'),
nama: 'Budi Santoso',
nim: '10112001',
alamat: { jalan: 'Jl. Kenanga No. 7', kota: 'Bandung', kodePos: '40135' }
}Satu permintaan findOne cukup karena alamat terbawa bersama profilnya. Tidak ada query kedua dan tidak ada tahap penggabungan di sisi aplikasi. Pembaruan alamat juga cukup satu updateOne pada satu dokumen. Perubahan seluruh field pada satu dokumen berlaku secara atomik, sehingga tidak ada keadaan ketika kota sudah berubah sedangkan kode posnya masih lama.
Field di dalam subdokumen tetap dapat diquery secara langsung dengan notasi titik, misalnya alamat.kota. MongoDB mencocokkan field bersarang sebagaimana field tingkat atas, sehingga data yang ditanam tetap dapat dicari tanpa membongkar strukturnya. Proyeksi juga berlaku pada subdokumen, sehingga halaman yang hanya membutuhkan alamat dapat memilih field alamat saja tanpa membawa field lainnya.
Referencing menyimpan ObjectId sebagai penghubung#
Referencing menyimpan ObjectId milik dokumen lain pada sebuah field, sehingga setiap entitas berdiri di koleksinya sendiri. Bentuk ini dipilih apabila data anak diakses secara terpisah, dibaca tanpa induknya, atau bertumbuh besar. Pada domain kampus, dosen adalah entitas mandiri yang memiliki jadwal mengajar dan publikasi, sehingga relasi dosen sebagai wali akademik mahasiswa dipetakan dengan rujukan.
db.dosen.insertMany([
{ nama: "Dr. Ali Rahman", bidang: "Statistika" },
{ nama: "Dr. Susi Andani", bidang: "Basis Data" }
]){
acknowledged: true,
insertedIds: {
'0': ObjectId('6ae8992ef71ce5016d8de667'),
'1': ObjectId('6ae8992ef71ce5016d8de668')
}
}const dosen = db.dosen.findOne({ nama: "Dr. Ali Rahman" })
db.mahasiswa.insertOne({ nama: "Dewi Lestari", nim: "10112002", waliId: dosen._id }){
acknowledged: true,
insertedId: ObjectId('6ae8992ef71ce5016d8de669')
}Field waliId memuat ObjectId milik dokumen dosen yang dirujuk. Relasi hanya tersambung penuh pada saat data dibaca, yaitu ketika query mencari dokumen dosen berdasarkan nilai waliId tersebut. Dokumen mahasiswa dan dokumen dosen diperbarui masing-masing di koleksinya sendiri, dan rujukan di antara keduanya tidak berubah oleh pembaruan tersebut.
Rujukan bekerja tanpa salinan data pada sisi mana pun. Dokumen dosen tidak menyimpan daftar mahasiswa yang diwalikan, karena daftar itu dapat dibaca dari arah lain dengan query db.mahasiswa.find({ waliId: ... }). Desain semacam ini menghindari dua sumber kebenaran, yaitu dua salinan data relasi yang sama yang harus dijaga tetap sinkron. Setiap relasi baru cukup ditulis pada dokumen yang menyimpan rujukannya, yaitu dokumen mahasiswa.
Membaca rujukan dengan dua langkah atau $lookup#
Pembacaan data hasil referencing membutuhkan langkah tambahan yang tidak ada pada embedding. Dua pola yang lazim adalah pembacaan dua langkah di sisi aplikasi dan penggabungan di dalam server dengan tahap $lookup. Keduanya menghasilkan data yang sama, sedangkan pemilihannya menyesuaikan jumlah dokumen yang dibaca.
const mhs = db.mahasiswa.findOne({ nim: "10112002" })
const wali = db.dosen.findOne({ _id: mhs.waliId })
({ mahasiswa: mhs.nama, wali: wali.nama, bidang: wali.bidang }){
mahasiswa: 'Dewi Lestari',
wali: 'Dr. Ali Rahman',
bidang: 'Statistika'
}db.mahasiswa.aggregate([
{ $match: { waliId: { $exists: true } } },
{ $lookup: {
from: "dosen",
localField: "waliId",
foreignField: "_id",
as: "wali"
} },
{ $unwind: "$wali" },
{ $project: { _id: 0, mahasiswa: "$nama", wali: "$wali.nama" } }
])[ { mahasiswa: 'Dewi Lestari', wali: 'Dr. Ali Rahman' } ]Tahap $lookup menggabungkan koleksi dosen ke dalam pipeline berdasarkan kecocokan waliId dengan _id, sehingga hasilnya sudah utuh tanpa langkah tambahan di aplikasi. Untuk satu dokumen, dua langkah findOne lebih ringkas karena hanya dua dokumen yang dibaca. Untuk laporan yang melibatkan banyak mahasiswa sekaligus, satu pipeline $lookup lebih efisien daripada mengulang dua langkah untuk setiap baris.
Kedua pola mengandalkan kesamaan nilai yang persis, bukan kesamaan nama field, sehingga waliId harus memuat ObjectId yang benar-benar menunjuk dokumen dosen. Apabila dokumen dosen yang dirujuk sudah dihapus, pola dua langkah mengembalikan null yang perlu diperiksa aplikasi, sedangkan tahap $unwind pada pipeline sebelumnya membuang baris mahasiswa tersebut. Perbedaan perilaku ini penting diketahui sebelum bentuk polanya dipilih.
Kriteria menimbang embedding dan referencing#
Pilihan di antara kedua strategi mengikuti pola akses, bukan kebiasaan memodelkan tabel. Tabel berikut merangkum perbedaan keduanya pada empat pertimbangan utama.
| Pertimbangan | Embedding | Referencing |
|---|---|---|
| Pembacaan | satu pembacaan dokumen | dua langkah atau $lookup |
| Kemandirian data | anak hanya bermakna bersama induk | anak diakses sendiri |
| Pertumbuhan | dibatasi ukuran dokumen | tumbuh di koleksinya sendiri |
| Pembaruan | satu dokumen, berlaku atomik | dua koleksi, masing-masing diperbarui |
Frekuensi akses bersama menjadi pertimbangan pertama. Data yang hampir selalu dibaca bersama induknya cocok ditanam, sedangkan data yang dibaca sendiri lebih baik dirujuk. Alamat mahasiswa muncul pada kartu mahasiswa dan halaman profil, sehingga embedding menghindari query tambahan pada setiap pembacaan tersebut.
Ukuran dan pertumbuhan menjadi pertimbangan kedua. Ukuran satu dokumen MongoDB dibatasi 16 MB, sehingga data yang jumlahnya tidak terkendali, misalnya log atau riwayat transaksi, tidak boleh ditanam pada induknya. Referencing menempatkan data seperti itu pada koleksi tersendiri yang dapat diindeks dan dibaca per bagian. Pembaruan menjadi pertimbangan ketiga, karena data yang ditanam ikut berubah bersama induknya dalam satu operasi atomik, sedangkan data yang dirujuk diperbarui di koleksinya masing-masing.
Kesalahan umum#
Kesalahan pertama adalah menanam data yang ternyata diakses terpisah. Dokumen mahasiswa yang menyertakan seluruh riwayat pembayaran membuat setiap pembacaan profil membawa data yang tidak dibutuhkan, dan ukuran dokumennya terus membengkak. Ukuran yang membengkak juga memperlambat pembacaan yang tidak membutuhkan riwayat pembayaran, karena MongoDB tetap membaca dokumen secara utuh. Perbaikannya adalah memindahkan data tersebut ke koleksi sendiri, kemudian merujuknya dengan ObjectId.
Kesalahan kedua adalah merujuk data yang selalu dibaca bersama. Profil dosen yang dirujuk dari setiap mata kuliah memaksa dua pembacaan pada halaman yang memang menampilkan keduanya. Biaya dua langkah semacam ini tidak terasa selama data masih kecil, sehingga masalah kinerjanya baru terlihat pada laporan yang membaca banyak baris sekaligus. Apabila data itu selalu tampil bersama dan tidak bertumbuh besar, penanaman menghasilkan bacaan yang lebih murah.
Kesalahan ketiga adalah menganggap referencing setara dengan foreign key. MongoDB tidak menegakkan rujukan secara otomatis, sehingga nilai waliId yang menunjuk dokumen dosen yang sudah dihapus tetap lolos saat ditulis. Pemeriksaan keberadaan rujukan sebelum penghapusan, atau pengalihan wali secara eksplisit, menjadi bagian dari operasi penghapusan dosen.
Ringkasan#
- Embedding menanam data terkait sebagai subdokumen atau array di dalam dokumen induk, sehingga satu pembacaan mengembalikan semuanya.
- Referencing menyimpan ObjectId dokumen lain dan menyambung relasi saat query melalui dua langkah pembacaan atau tahap
$lookup. - Embedding menguntungkan data yang dibaca bersama, jarang berubah, dan tidak bertumbuh besar, sedangkan referencing menguntungkan data yang diakses sendiri atau tumbuh besar.
- Batas ukuran dokumen 16 MB dan sifat atomik pembaruan satu dokumen menjadi dasar penimbangan strategi.
Referensi#
- Data Modeling Introduction — MongoDB Manual — Pengantar keputusan pemodelan data dokumen, termasuk kapan data ditanam atau dirujuk.
- Data Model Design — MongoDB Manual — Pembahasan denormalisasi dan normalisasi beserta faktor yang menentukannya.
- $lookup — MongoDB Manual — Dokumentasi tahap penggabungan antarkoleksi di dalam pipeline.
- MongoDB Limits and Thresholds — MongoDB Manual — Daftar batas MongoDB, termasuk batas ukuran dokumen 16 MB.