Relasi satu-ke-banyak muncul ketika satu dokumen induk berkaitan dengan beberapa dokumen anak, misalnya satu dosen dengan beberapa mata kuliah yang diampu. MongoDB menyediakan tiga bentuk pemodelan untuk relasi ini, yaitu subdokumen yang ditanam pada induk, array of ObjectId pada induk, dan rujukan yang disimpan pada dokumen anak. Topik ini membahas pemilihan ketiganya berdasarkan jumlah anak, kemandirian akses, dan pola pertumbuhan pada basis data kampus. Pembahasan mencakup query dua arah, yaitu query anak dari induk dan query induk dari anak.
Subdokumen untuk relasi satu-ke-sedikit#
Relasi satu-ke-sedikit adalah relasi yang anaknya berjumlah sedikit, hanya bermakna di dalam induknya, dan tidak pernah dibaca secara terpisah. Bentuk yang tepat untuk pola ini adalah array subdokumen yang ditanam pada dokumen induk. Contohnya adalah riwayat pendidikan seorang dosen, yang hanya dibaca bersama profil dosen itu sendiri dan tidak bertambah setelah riwayatnya ditetapkan.
use kampusswitched to db kampusdb.dosen.insertOne({
nama: "Dr. Ali Rahman",
bidang: "Statistika",
pendidikan: [
{ jenjang: "S1", institusi: "Universitas Nusantara", tahun: 2005 },
{ jenjang: "S2", institusi: "Institut Teknologi Cendana", tahun: 2008 },
{ jenjang: "S3", institusi: "Universitas Cakrawala", tahun: 2013 }
]
}){
acknowledged: true,
insertedId: ObjectId('6ae899686d6d05e0b08de666')
}db.dosen.findOne({ nama: "Dr. Ali Rahman" }, { _id: 0 }){
nama: 'Dr. Ali Rahman',
bidang: 'Statistika',
pendidikan: [
{ jenjang: 'S1', institusi: 'Universitas Nusantara', tahun: 2005 },
{
jenjang: 'S2',
institusi: 'Institut Teknologi Cendana',
tahun: 2008
},
{ jenjang: 'S3', institusi: 'Universitas Cakrawala', tahun: 2013 }
]
}Satu pembacaan mengembalikan profil dosen lengkap dengan seluruh riwayat pendidikannya. Karena jumlah anaknya sedikit dan tidak bertambah, array pendidikan tidak akan membengkak. Kebutuhan membaca riwayat pendidikan tanpa profil dosen juga tidak ada, sehingga biaya embedding untuk pola ini kecil dan tidak menimbulkan bacaan yang sia-sia.
Pembaruan pada satu anak pun tetap berada di dalam dokumen induk. Menambahkan satu riwayat pendidikan berarti satu updateOne dengan $push pada array pendidikan, dan perubahan itu berlaku atomik bersama induknya. Array seperti ini juga tidak memerlukan indeks tambahan, karena isinya hanya dibaca bersama induk dan tidak pernah dicocokkan dari arah lain. Batas 16 MB per dokumen pun tetap aman, karena array yang elemennya sedikit dan isinya tidak bertambah tidak akan mendekati plafon tersebut.
Array of ObjectId pada induk untuk relasi moderat#
Bentuk kedua menaruh daftar _id anak pada dokumen induk, yang lazim disebut array of ObjectId. Bentuk ini cocok untuk relasi yang anaknya berjumlah puluhan sampai ratusan, sering dibaca sebagai satu kelompok, dan hampir selalu diakses melalui induknya. Contohnya adalah program studi beserta daftar mahasiswanya, yang jumlahnya terkendali oleh aturan akademik dan selalu ditampilkan bersama halaman program studi.
db.prodi.insertOne({ _id: "STAT", nama: "Statistika", jenjang: "S1" }){ acknowledged: true, insertedId: 'STAT' }db.mahasiswa.insertMany([
{ _id: "10115001", nama: "Budi Santoso" },
{ _id: "10115002", nama: "Citra Melati" },
{ _id: "10115003", nama: "Dewi Lestari" },
{ _id: "10115004", nama: "Eka Pratama" },
{ _id: "10115005", nama: "Fitri Handayani" },
{ _id: "10115006", nama: "Gilang Saputra" }
]){
acknowledged: true,
insertedIds: {
'0': '10115001',
'1': '10115002',
'2': '10115003',
'3': '10115004',
'4': '10115005',
'5': '10115006'
}
}Nilai _id mahasiswa diisi NIM agar daftar rujukannya mudah dibaca, dan ObjectId tetap bekerja seperti biasa pada pola ini. Dokumen induk kemudian menyimpan daftar tersebut sebagai satu array.
db.prodi.updateOne(
{ _id: "STAT" },
{ $set: { mahasiswaId: [ "10115001", "10115002", "10115003", "10115004", "10115005", "10115006" ] } }
){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}Pembacaan mahasiswa satu program studi menggunakan operator $in terhadap array tersebut.
const prodi = db.prodi.findOne({ _id: "STAT" })
db.mahasiswa.find({ _id: { $in: prodi.mahasiswaId } }, { _id: 1, nama: 1 })[
{ _id: '10115001', nama: 'Budi Santoso' },
{ _id: '10115002', nama: 'Citra Melati' },
{ _id: '10115003', nama: 'Dewi Lestari' },
{ _id: '10115004', nama: 'Eka Pratama' },
{ _id: '10115005', nama: 'Fitri Handayani' },
{ _id: '10115006', nama: 'Gilang Saputra' }
]Daftar anak ditemukan langsung dari induk, lalu operator $in menerjemahkan daftar itu menjadi satu query ke koleksi mahasiswa. Query seperti ini tidak perlu menyapu koleksi mahasiswa untuk mencari keanggotaannya, karena daftarnya sudah tersimpan pada dokumen prodi.
Biaya pemeliharaan bentuk ini berada pada operasi penulisan. Setiap mahasiswa yang bergabung atau pindah program studi mengubah dokumen prodi, sehingga perubahan keanggotaan tidak pernah gratis. Apabila perubahan itu jarang terjadi, biayanya sepadan dengan pembacaan kelompok yang murah, sedangkan keanggotaan yang berubah terus-menerus lebih cocok ditangani dengan bentuk rujukan di dokumen anak.
Rujukan pada dokumen anak untuk relasi yang banyak#
Bentuk ketiga menaruh rujukan pada dokumen anak, yaitu setiap anak menyimpan _id induknya. Bentuk ini paling aman apabila jumlah anaknya besar atau sulit ditaksir, karena dokumen induk tidak pernah membawa array yang tumbuh terus. Contohnya adalah mata kuliah yang diampu seorang dosen, karena jumlahnya bisa bertambah dari tahun ke tahun tanpa mengubah ukuran dokumen dosen.
const dosenWali = db.dosen.findOne({ nama: "Dr. Ali Rahman" })
db.mata_kuliah.insertMany([
{ kode: "TSA201", nama: "Statistika Terapan", sks: 3, dosenId: dosenWali._id },
{ kode: "TSA301", nama: "Analisis Regresi", sks: 3, dosenId: dosenWali._id }
]){
acknowledged: true,
insertedIds: {
'0': ObjectId('6ae899686d6d05e0b08de667'),
'1': ObjectId('6ae899686d6d05e0b08de668')
}
}Query anak dari induk cukup satu find dengan syarat field rujukan, sedangkan query induk dari anak cukup satu pembacaan.
db.mata_kuliah.find(
{ dosenId: db.dosen.findOne({ nama: "Dr. Ali Rahman" })._id },
{ _id: 0, kode: 1, nama: 1 }
)[
{ kode: 'TSA201', nama: 'Statistika Terapan' },
{ kode: 'TSA301', nama: 'Analisis Regresi' }
]const mk = db.mata_kuliah.findOne({ kode: "TSA301" })
db.dosen.findOne({ _id: mk.dosenId }, { _id: 0, nama: 1, bidang: 1 }){ nama: 'Dr. Ali Rahman', bidang: 'Statistika' }Field rujukan pada dokumen anak sebaiknya diindeks, karena pencarian anak dari induk adalah query yang paling sering berjalan pada pola ini. Kode TSA201 tetap tersimpan sebagai pengenal yang mudah dibaca, sedangkan rujukan antarkoleksi tetap menggunakan _id ObjectId yang tidak berubah meski kode atau nama diperbarui.
db.mata_kuliah.createIndex({ dosenId: 1 })dosenId_1Kesalahan umum#
Kesalahan pertama adalah menanam subdokumen yang ternyata diakses terpisah. Riwayat pendidikan yang ditanam pada dokumen dosen aman, tetapi daftar publikasi yang sering dicari tanpa profil dosen membuat setiap query publikasi membaca data yang tidak diperlukan. Perbaikannya adalah memindahkan anak tersebut ke koleksi sendiri dengan field rujukan.
Kesalahan kedua adalah memilih array pada induk untuk relasi yang jumlah anaknya tidak terbatas. Array mahasiswaId yang berisi enam nilai mudah dibaca, sedangkan array yang harus menampung setiap entri kehadiran selama bertahun-tahun membuat dokumen induk tumbuh tanpa kendali. Pemilihan bentuk perlu menghitung pertumbuhan selama beberapa tahun, bukan hanya jumlah awalnya. Perhitungan yang sama berlaku untuk arsip dan log aktivitas yang elemennya terus bertambah selama aplikasi berjalan.
Kesalahan ketiga adalah menyimpan rujukan pada dua arah sekaligus tanpa disiplin. Dokumen anak yang memuat dosenId dan dokumen induk yang memuat array mataKuliahId memerlukan dua pembaruan untuk satu perubahan keanggotaan, dan salah satu pembaruan yang gagal membuat kedua sisi tidak konsisten. Tetapkan satu arah sebagai sumber kebenaran, lalu baca arah lainnya melalui query.
Ringkasan#
- Relasi satu-ke-sedikit dipetakan dengan array subdokumen pada induk, sehingga satu pembacaan mengembalikan induk beserta seluruh anaknya.
- Relasi moderat yang dibaca sebagai satu kelompok dipetakan dengan array of ObjectId pada induk dan dibaca dengan operator
$in. - Relasi yang jumlah anaknya besar atau tidak terbatas dipetakan dengan rujukan pada dokumen anak, disertai indeks pada field rujukan.
- Array pada induk hanya aman selama jumlah anaknya terkendali, dan rujukan pada dua arah sekaligus menimbulkan risiko inkonsistensi.
Referensi#
- Model One-to-Many Relationships with Embedded Documents — MongoDB Manual — Pola penanaman subdokumen untuk relasi satu-ke-banyak.
- Model One-to-Many Relationships with Document References — MongoDB Manual — Pola rujukan dokumen anak dan dokumen induk untuk relasi satu-ke-banyak.
- $in — MongoDB Manual — Operator pencocokan nilai terhadap daftar pada satu query.
- $set — MongoDB Manual — Operator pembaruan field, termasuk mengisi array rujukan pada dokumen induk.