Memodelkan satu-ke-banyak

Memilih antara subdokumen, array, dan referensi untuk relasi satu-ke-banyak.

Daftar isi

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.

mongosh
use kampus
text Keluaran
switched to db kampus
mongosh
db.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 }
  ]
})
text Keluaran
{
  acknowledged: true,
  insertedId: ObjectId('6ae899686d6d05e0b08de666')
}
mongosh
db.dosen.findOne({ nama: "Dr. Ali Rahman" }, { _id: 0 })
text Keluaran
{
  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.

mongosh
db.prodi.insertOne({ _id: "STAT", nama: "Statistika", jenjang: "S1" })
text Keluaran
{ acknowledged: true, insertedId: 'STAT' }
mongosh
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" }
])
text Keluaran
{
  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.

mongosh
db.prodi.updateOne(
  { _id: "STAT" },
  { $set: { mahasiswaId: [ "10115001", "10115002", "10115003", "10115004", "10115005", "10115006" ] } }
)
text Keluaran
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

Pembacaan mahasiswa satu program studi menggunakan operator $in terhadap array tersebut.

mongosh
const prodi = db.prodi.findOne({ _id: "STAT" })
db.mahasiswa.find({ _id: { $in: prodi.mahasiswaId } }, { _id: 1, nama: 1 })
text Keluaran
[
  { _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.

mongosh
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 }
])
text Keluaran
{
  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.

mongosh
db.mata_kuliah.find(
  { dosenId: db.dosen.findOne({ nama: "Dr. Ali Rahman" })._id },
  { _id: 0, kode: 1, nama: 1 }
)
text Keluaran
[
  { kode: 'TSA201', nama: 'Statistika Terapan' },
  { kode: 'TSA301', nama: 'Analisis Regresi' }
]
mongosh
const mk = db.mata_kuliah.findOne({ kode: "TSA301" })
db.dosen.findOne({ _id: mk.dosenId }, { _id: 0, nama: 1, bidang: 1 })
text Keluaran
{ 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.

mongosh
db.mata_kuliah.createIndex({ dosenId: 1 })
text Keluaran
dosenId_1

Kesalahan 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#