Katalog toko buku modern tidak hanya menjual buku cetak: ada ebook, ada audiobook. Ketiganya adalah produk — punya nama, harga, penulis — tetapi field khasnya berbeda: buku punya jumlahHalaman, ebook punya ukuranMB, audiobook punya narator. Polymorphic Pattern menjawab situasi ini dengan satu collection untuk semua bentuk, plus satu field pembeda bernama type. Alternatifnya — satu collection per bentuk — membuat query lintas jenis dan index menjadi berlipat kali lebih rumit.

Satu Collection, Banyak Bentuk #

Field pembeda (type) adalah jangkar pola ini: satu field yang selalu hadir dan menjadi penentu bentuk document:

js
use latihan_modeling
db.dropDatabase()
db.produk.insertMany([
  { type: "buku", nama: "Bumi", penulis: "Tere Liye", harga: 89000, jumlahHalaman: 440, beratGram: 350 },
  { type: "buku", nama: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, jumlahHalaman: 380, beratGram: 300 },
  { type: "ebook", nama: "Filosofi Teras", penulis: "Henry Manampiring", harga: 65000, format: "EPUB", ukuranMB: 2.8 },
  { type: "audiobook", nama: "Atomic Habits", penulis: "James Clear", harga: 119000, durasiMenit: 320, narator: "Ari Purnama" }
])
db.produk.find({ type: "ebook" })

Output yang Dihasilkan:

text
switched to db latihan_modeling
{ ok: 1, dropped: 'latihan_modeling' }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad39a5632abc455164aaab3'),
    '1': ObjectId('6ad39a5632abc455164aaab4'),
    '2': ObjectId('6ad39a5632abc455164aaab5'),
    '3': ObjectId('6ad39a5632abc455164aaab6')
  }
}
[
  {
    _id: ObjectId('6ad39a5632abc455164aaab5'),
    type: 'ebook',
    nama: 'Filosofi Teras',
    penulis: 'Henry Manampiring',
    harga: 65000,
    format: 'EPUB',
    ukuranMB: 2.8
  }
]

Empat document, tiga bentuk, satu collection. Document ebook tidak punya jumlahHalaman; document buku tidak punya ukuranMB — schema MongoDB yang fleksibel membiarkan perbedaan ini hidup berdampingan tanpa nilai kosong.

Bedah peran tiap bagian:

  1. type sebagai penanda bentuk — satu-satunya field yang wajib hadir di semua document; aplikasi membacanya lebih dulu untuk tahu field apa lagi yang boleh diharapkan.
  2. Field bersama bernama seragam — nama, penulis, dan harga pakai nama yang sama di semua bentuk; inilah yang membuat query lintas bentuk mungkin.
  3. Field khas milik bentuknya — format, narator, jumlahHalaman hanya muncul pada bentuk yang relevan; tidak ada field jumlahHalaman: null yang mengotori document ebook.

Menambah bentuk baru pun tidak menuntut migrasi: besok toko menjual alat-tulis, cukup insert document dengan type: "alat-tulis" beserta field khasnya, dan seluruh query katalog tetap bekerja. Konsekuensinya ada di sisi kode — setiap cabang aplikasi yang membaca field khas wajib punya rencana untuk type yang belum dikenal (abaikan, lewati, atau tampilkan generik), karena database tidak akan menolak bentuk asing yang masuk.

Query Lintas Bentuk #

Kekuatan pola ini terlihat saat query bergerak antar bentuk. Field bersama bisa ditanyakan tanpa peduli type; field khas tinggal difilter ke bentuknya:

js
db.produk.find({ harga: { $lt: 90000 } }, { nama: 1, harga: 1 })
db.produk.find({ type: "buku", jumlahHalaman: { $gt: 400 } }, { nama: 1 })
db.produk.countDocuments({ type: "audiobook" })

Output yang Dihasilkan:

text
[
  {
    _id: ObjectId('6ad39a5632abc455164aaab3'),
    nama: 'Bumi',
    harga: 89000
  },
  {
    _id: ObjectId('6ad39a5632abc455164aaab5'),
    nama: 'Filosofi Teras',
    harga: 65000
  }
]
[ { _id: ObjectId('6ad39a5632abc455164aaab3'), nama: 'Bumi' } ]
1

Tiga query berturut-turut: filter harga menangkap ebook dan buku tanpa perbedaan perlakuan, filter jumlahHalaman secara alami hanya relevan untuk buku, dan penghitungan audiobook cukup mengandalkan type. Satu index pada type melayani semua slicing per bentuk, dan index gabungan seperti { type: 1, harga: 1 } membuat “ebook termurah” menjadi satu query ber-index:

js
db.produk.createIndex({ type: 1, harga: 1 })
db.produk.aggregate([
  { $group: { _id: "$type", jumlah: { $sum: 1 }, hargaRata: { $avg: "$harga" } } },
  { $set: { hargaRata: { $round: ["$hargaRata", 0] } } },
  { $sort: { _id: 1 } }
])

Output yang Dihasilkan:

text
type_1_harga_1
[
  { _id: 'audiobook', jumlah: 1, hargaRata: 119000 },
  { _id: 'buku', jumlah: 2, hargaRata: 93500 },
  { _id: 'ebook', jumlah: 1, hargaRata: 65000 }
]

Keuntungan dan Risiko #

KeuntunganRisiko
Query lintas jenis dalam satu findKode aplikasi harus tahu cara membaca tiap type
Satu index pada type melayani semua bentukValidasi schema jadi lebih longgar atau rumit
Bentuk baru cukup type baru — tanpa collection baruField khas mudah tertukar antar bentuk (typo tak terdeteksi)
Halaman katalog seragam untuk semua produkSatu bentuk bermasalah membebani collection bersama

Disiplin yang menjaga pola ini tetap sehat: type wajib hadir di semua document, field bersama diberi nama identik di semua bentuk, dan setiap bentuk punya kontrak field yang terdokumentasi — pola Schema Versioning dari chapter sebelumnya bisa dipakai bila salah satu bentuk berevolusi.

Perbandingan dengan alternatifnya memperjelas posisi pola ini. Tiga collection terpisah (buku, ebook, audiobook) memberi validasi paling ketat per bentuk, tetapi halaman katalog harus menggabungkan tiga query dan tiga index; satu collection polymorphic menukar ketatnya validasi dengan kelancaran query lintas jenis. Katalog etalase yang dibaca jauh lebih sering daripada ditulis hampir selalu memilih yang kedua.

Rangkuman #

  • Polymorphic Pattern menyimpan beberapa bentuk entitas dalam satu collection dengan field pembeda type.
  • Field bersama dilayani lintas bentuk; field khas difilter pada bentuknya; index { type: 1, ... } melayani keduanya.
  • Keuntungan utamanya adalah query lintas jenis dan penambahan bentuk baru tanpa collection baru.
  • Risikonya bergeser ke aplikasi dan validasi: kode harus mengenali tiap bentuk, dan validator perlu anyOf/oneOf.

Bentuk berbeda dalam satu collection adalah satu cara menghadapi variasi. Chapter berikutnya menghadapi variasi yang lain: data yang jumlahnya tak sebanding antar document — Subset Pattern.