Field array seperti tag pada koleksi buku menyimpan banyak nilai sekaligus, dan pertanyaannya pun ikut beragam: “buku dengan kedua tag ini sekaligus”, “buku dengan tepat tiga tag”, atau “buku yang punya satu tag yang memenuhi dua syarat tertentu”. Query biasa hanya sanggup menjawab sebagian. MongoDB menyediakan tiga operator khusus array untuk sisanya — $all, $size, dan $elemMatch — dan yang terakhir adalah satu-satunya cara benar untuk menguji beberapa kondisi pada elemen yang sama. Chapter ini menguji ketiganya dengan data yang sama seperti Chapter sebelumnya.
Peta materi Chapter ini:
$all: array harus memuat semua nilai yang diminta.$size: array harus memiliki panjang persis.$elemMatch: satu elemen harus memenuhi beberapa kondisi sekaligus.
Data Uji #
Kita pakai koleksi buku yang sama, dengan dua penyesuaian: field tag kini lebih bervariasi ukurannya, dan ada field ulasan berisi array of documents untuk ujian $elemMatch nanti:
use latihan_crud
db.buku.drop()Output yang Dihasilkan:
switched to db latihan_crud
truedb.buku.insertMany([
{ judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, tag: ["fiksi", "sastra", "pengarang Indonesia"], ulasan: [ { pembaca: "Budi", bintang: 5 }, { pembaca: "Siti", bintang: 4 } ] },
{ judul: "Bumi", penulis: "Tere Liye", harga: 89000, tag: ["fiksi", "remaja"], ulasan: [ { pembaca: "Budi", bintang: 4 }, { pembaca: "Dewi", bintang: 5 } ] },
{ judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, tag: ["nonfiksi", "filsafat", "pengembangan diri", "stoa"], ulasan: [ { pembaca: "Siti", bintang: 5 } ] },
{ judul: "Atomic Habits", penulis: "James Clear", harga: 115000, tag: ["nonfiksi", "pengembangan diri"], ulasan: [ { pembaca: "Dewi", bintang: 3 }, { pembaca: "Budi", bintang: 4 }, { pembaca: "Siti", bintang: 5 } ] },
{ judul: "Kamus Bahasa Indonesia", penulis: "Badan Bahasa", harga: 55000, tag: ["referensi"] }
])Output yang Dihasilkan:
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3892ecf0dbcd6260475c4'),
'1': ObjectId('6ad3892ecf0dbcd6260475c5'),
'2': ObjectId('6ad3892ecf0dbcd6260475c6'),
'3': ObjectId('6ad3892ecf0dbcd6260475c7'),
'4': ObjectId('6ad3892ecf0dbcd6260475c8')
}
}$all: Memuat Semua Nilai #
Operator $all mencocokkan document yang array-nya memuat semua nilai yang diminta, tanpa peduli urutan dan tanpa melarang nilai lain. Bandingkan dua query berikut:
db.buku.find({ tag: ["fiksi", "remaja"] }, { judul: 1, _id: 0 })
db.buku.find({ tag: { $all: ["fiksi", "remaja"] } }, { judul: 1, _id: 0 })Output yang Dihasilkan:
[ { judul: 'Bumi' } ]
[ { judul: 'Bumi' } ]Dua-duanya mengembalikan Bumi, tetapi lewat jalur yang berbeda. Mari kita bedah:
- Baris pertama (kesamaan array): filter
{ tag: ["fiksi", "remaja"] }mencocokkan array secara persis sama — elemen yang sama, urutan yang sama, jumlah yang sama.Bumilolos karenatag-nya memang tepat["fiksi", "remaja"]. - Baris kedua ($all): filter mencari document yang
tag-nya memuatfiksidanremaja, di urutan mana pun, dengan anggota lain tetap diperbolehkan.Bumisatu-satunya yang memuat keduanya —Laut Berceritapunyafiksitetapi tidakremaja.
Perbedaan ini terasa saat daftar nilai diminta dalam urutan lain:
db.buku.find({ tag: ["remaja", "fiksi"] }, { judul: 1, _id: 0 })
db.buku.find({ tag: { $all: ["remaja", "fiksi"] } }, { judul: 1, _id: 0 })Output yang Dihasilkan:
[]
[ { judul: 'Bumi' } ]Kesamaan array menolak Bumi karena urutannya terbalik, sedangkan $all tetap menemukannya. Gunakan $all ketika urutan tidak penting; gunakan kesamaan array ketika urutan adalah bagian dari makna data.
$size: Panjang Array yang Persis #
Operator $size mencocokkan document yang array-nya memiliki jumlah elemen persis sebesar angka yang diminta:
db.buku.find({ tag: { $size: 1 } }, { judul: 1, _id: 0 })Output yang Dihasilkan:
[ { judul: 'Kamus Bahasa Indonesia' } ]Hanya Kamus Bahasa Indonesia yang memiliki tepat satu tag. Sepanjang daftar kita: Bumi dan Atomic Habits berukuran dua, Laut Bercerita tiga, dan Filosofi Teras empat.
Batasan penting $size terletak pada namanya: operator ini hanya menerima angka pasti. MongoDB tidak menyediakan $size dengan rentang seperti { $size: { $gt: 2 } }. Untuk “minimal dua tag”, solusinya adalah kombinasi $exists dan $elemMatch? Bukan — cara yang benar adalah menyimpan panjang array sebagai field terpisah saat menulis, atau memakai aggregation dengan $size (operator ekspresi, berbeda dari operator query ini) yang akan kita pelajari di Section Aggregation.
$elemMatch: Satu Elemen, Banyak Syarat #
Operator inilah bintang chapter ini. $elemMatch menuntut satu elemen array yang sama memenuhi semua kondisi yang ditulis. Untuk melihat kenapa operator ini dibutuhkan, coba jawab dulu pertanyaan berikut dengan cara naif: “buku yang pernah diberi bintang 4 oleh Budi”.
Field ulasan adalah array of documents — pembaca dan bintang berpasangan di dalam satu ulasan. Pendekatan pertama menaruh dua kondisi langsung pada path bertitik:
db.buku.find({ "ulasan.pembaca": "Budi", "ulasan.bintang": 4 }, { judul: 1, _id: 0 })Output yang Dihasilkan:
[
{ judul: 'Laut Bercerita' },
{ judul: 'Bumi' },
{ judul: 'Atomic Habits' }
]Tiga buku kembali — dan salah satunya, Laut Bercerita, sebenarnya tidak pernah diberi bintang 4 oleh Budi. Query ini tidak menjamin pasangan pembaca dan bintang datang dari elemen ulasan yang sama. Ia hanya berkata: “ada ulasan oleh Budi, dan ada ulasan bintang 4” — dua kecocokan yang boleh terjadi di elemen berbeda. Anggap saja kita belum menyadarinya, dan lanjutkan dengan data tambahan:
db.buku.insertOne({ judul: "Uji Kejutan", penulis: "Anonim", harga: 10000, tag: ["penguji"], ulasan: [ { pembaca: "Budi", bintang: 5 }, { pembaca: "Dewi", bintang: 4 } ] })Output yang Dihasilkan:
{
acknowledged: true,
insertedId: ObjectId('6ad3892ed6f8b04e19b1ca19')
}Budi memberi buku ini bintang 5, dan tidak ada yang memberi bintang 4 atas nama Budi. Query naif tetap mengembalikannya:
db.buku.find({ "ulasan.pembaca": "Budi", "ulasan.bintang": 4 }, { judul: 1, _id: 0 })Output yang Dihasilkan:
[
{ judul: 'Laut Bercerita' },
{ judul: 'Bumi' },
{ judul: 'Atomic Habits' },
{ judul: 'Uji Kejutan' }
]Uji Kejutan kini ikut lolos — padahal Budi memberinya bintang 5, dan bintang 4 berasal dari Dewi. Kondisi pembaca: "Budi" terpenuhi oleh elemen pertama dan bintang: 4 oleh elemen kedua. Dua buku — Laut Bercerita dan Uji Kejutan — adalah positif palsu dari query yang sama. Inilah jebakan klasik query array: MongoDB mencocokkan tiap kondisi terhadap seluruh elemen, lalu menggabungkan hasilnya.
$elemMatch menutup celah ini dengan mengunci semua kondisi pada satu elemen:
db.buku.find({ ulasan: { $elemMatch: { pembaca: "Budi", bintang: 4 } } }, { judul: 1, _id: 0 })Output yang Dihasilkan:
[ { judul: 'Bumi' }, { judul: 'Atomic Habits' } ]Sekarang kedua positif palsu hilang sekaligus: Laut Bercerita dan Uji Kejutan tidak lagi muncul karena tidak ada satu ulasan yang sekaligus bernama Budi dan berbintang 4. Mari kita bedah bentuknya:
ulasan(field array): operator$elemMatchditempelkan langsung pada field array, bukan pada path elemennya.{ pembaca: "Budi", bintang: 4 }: isi$elemMatchberbentuk filter biasa; seluruh filter ini diuji terhadap tiap elemen secara individual.- Semantik kecocokan: document lolos bila minimal satu elemen lolos seluruh filter.
Bersihkan buku penguji sebelum lanjut:
db.buku.deleteOne({ judul: "Uji Kejutan" })Output yang Dihasilkan:
{ acknowledged: true, deletedCount: 1 }Kapan Memakai Apa #
| Kebutuhan | Operator |
|---|---|
| Array memuat semua nilai ini (urutan bebas) | $all |
| Array berukuran tepat N | $size |
| Satu elemen memenuhi beberapa kondisi | $elemMatch |
| Minimal satu nilai ada di array | $in atau kesamaan langsung |
| Setiap elemen memenuhi kondisi | hanya via aggregation ($allElementsTrue) |
Rangkuman #
$allmenuntut semua nilai hadir dalam array, mengabaikan urutan dan anggota lain.$sizemencocokkan panjang array secara persis, tidak mendukung rentang, dan tidak efisien di index.$elemMatchmengunci beberapa kondisi pada satu elemen array — wajib untuk array of documents.- Query multi-path pada array (misalnya
"ulasan.pembaca"dan"ulasan.bintang") dicocokkan lintas elemen dan bisa menghasilkan positif palsu.