Tentu saja ada beberapa pendekatan tergantung pada versi MongoDB Anda yang tersedia. Ini bervariasi dari penggunaan yang berbeda dari $lookup
melalui untuk mengaktifkan manipulasi objek pada .populate()
hasil melalui .lean()
.
Saya meminta Anda membaca bagian ini dengan cermat, dan berhati-hatilah bahwa semua mungkin tidak seperti yang terlihat saat mempertimbangkan solusi implementasi Anda.
MongoDB 3.6, $lookup "bersarang"
Dengan MongoDB 3.6 $lookup
operator mendapatkan kemampuan tambahan untuk menyertakan pipeline
ekspresi daripada hanya menggabungkan nilai kunci "lokal" ke "asing", artinya Anda pada dasarnya dapat melakukan setiap $lookup
sebagai "bersarang" dalam ekspresi saluran ini
Venue.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
{ "$lookup": {
"from": Review.collection.name,
"let": { "reviews": "$reviews" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
{ "$lookup": {
"from": Comment.collection.name,
"let": { "comments": "$comments" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
{ "$lookup": {
"from": Author.collection.name,
"let": { "author": "$author" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
{ "$addFields": {
"isFollower": {
"$in": [
mongoose.Types.ObjectId(req.user.id),
"$followers"
]
}
}}
],
"as": "author"
}},
{ "$addFields": {
"author": { "$arrayElemAt": [ "$author", 0 ] }
}}
],
"as": "comments"
}},
{ "$sort": { "createdAt": -1 } }
],
"as": "reviews"
}},
])
Ini bisa benar-benar sangat kuat, seperti yang Anda lihat dari perspektif pipa asli, itu benar-benar hanya tahu tentang menambahkan konten ke "reviews"
array dan kemudian setiap ekspresi saluran pipa "bersarang" berikutnya juga hanya melihat elemen "dalam" dari gabungan.
Ini kuat dan dalam beberapa hal mungkin sedikit lebih jelas karena semua jalur bidang relatif terhadap tingkat bersarang, tetapi itu memulai lekukan yang merayap dalam struktur BSON, dan Anda perlu menyadari apakah Anda cocok dengan array atau nilai tunggal dalam melintasi struktur.
Perhatikan bahwa kami juga dapat melakukan hal-hal di sini seperti "meratakan properti penulis" seperti yang terlihat dalam "comments"
entri array. Semua $lookup
keluaran target mungkin berupa "array", tetapi dalam "sub-pipeline" kita dapat membentuk kembali larik elemen tunggal itu menjadi hanya satu nilai.
Standar MongoDB $lookup
Masih menyimpan "bergabung di server" Anda sebenarnya dapat melakukannya dengan $lookup
, tetapi hanya membutuhkan pemrosesan menengah. Ini adalah pendekatan lama dengan mendekonstruksi array dengan $unwind
dan menggunakan $group
tahapan untuk membangun kembali array:
Venue.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
{ "$lookup": {
"from": Review.collection.name,
"localField": "reviews",
"foreignField": "_id",
"as": "reviews"
}},
{ "$unwind": "$reviews" },
{ "$lookup": {
"from": Comment.collection.name,
"localField": "reviews.comments",
"foreignField": "_id",
"as": "reviews.comments",
}},
{ "$unwind": "$reviews.comments" },
{ "$lookup": {
"from": Author.collection.name,
"localField": "reviews.comments.author",
"foreignField": "_id",
"as": "reviews.comments.author"
}},
{ "$unwind": "$reviews.comments.author" },
{ "$addFields": {
"reviews.comments.author.isFollower": {
"$in": [
mongoose.Types.ObjectId(req.user.id),
"$reviews.comments.author.followers"
]
}
}},
{ "$group": {
"_id": {
"_id": "$_id",
"reviewId": "$review._id"
},
"name": { "$first": "$name" },
"addedBy": { "$first": "$addedBy" },
"review": {
"$first": {
"_id": "$review._id",
"createdAt": "$review.createdAt",
"venue": "$review.venue",
"author": "$review.author",
"content": "$review.content"
}
},
"comments": { "$push": "$reviews.comments" }
}},
{ "$sort": { "_id._id": 1, "review.createdAt": -1 } },
{ "$group": {
"_id": "$_id._id",
"name": { "$first": "$name" },
"addedBy": { "$first": "$addedBy" },
"reviews": {
"$push": {
"_id": "$review._id",
"venue": "$review.venue",
"author": "$review.author",
"content": "$review.content",
"comments": "$comments"
}
}
}}
])
Ini sebenarnya tidak menakutkan seperti yang Anda pikirkan pada awalnya dan mengikuti pola sederhana $lookup
dan $unwind
saat Anda maju melalui setiap larik.
"author"
detail tentu saja tunggal, jadi setelah "dibatalkan" Anda hanya ingin membiarkannya seperti itu, buat penambahan bidang dan mulai proses "memutar kembali" ke dalam array.
Hanya ada dua level untuk direkonstruksi kembali ke Venue
asli dokumen, jadi level detail pertama adalah dengan Review
untuk membangun kembali "comments"
Himpunan. Anda hanya perlu $push
jalur "$reviews.comments"
untuk mengumpulkan ini, dan selama "$reviews._id"
bidang ada di "pengelompokan _id" satu-satunya hal lain yang perlu Anda simpan adalah semua bidang lainnya. Anda dapat memasukkan semua ini ke dalam _id
juga, atau Anda dapat menggunakan $first
.
Setelah itu selesai, hanya ada satu $group
panggung untuk kembali ke Venue
diri. Kali ini kunci pengelompokannya adalah "$_id"
tentu saja, dengan semua properti tempat itu sendiri menggunakan $first
dan "$review"
yang tersisa detail kembali ke array dengan $push
. Tentu saja "$comments"
keluaran dari $group
previous sebelumnya menjadi "review.comments"
jalan.
Bekerja pada satu dokumen dan hubungannya, ini tidak terlalu buruk. $unwind
operator pipa dapat umumnya menjadi masalah kinerja, tetapi dalam konteks penggunaan ini seharusnya tidak terlalu menimbulkan dampak sebesar itu.
Karena data masih "bergabung di server" masih ada masih lalu lintas jauh lebih sedikit daripada alternatif lain yang tersisa.
Manipulasi JavaScript
Tentu saja kasus lain di sini adalah bahwa alih-alih mengubah data di server itu sendiri, Anda sebenarnya memanipulasi hasilnya. Di sebagian besar kasus saya akan mendukung pendekatan ini karena "tambahan" apa pun pada data mungkin paling baik ditangani pada klien.
Masalahnya tentu saja dengan menggunakan populate()
adalah bahwa meskipun mungkin 'terlihat seperti' proses yang jauh lebih disederhanakan, ini sebenarnya BUKAN GABUNG dengan cara apapun. Semua populate()
sebenarnya adalah "sembunyikan" proses yang mendasari pengiriman beberapa kueri ke database, lalu menunggu hasilnya melalui penanganan asinkron.
Jadi "penampilan" dari gabungan sebenarnya adalah hasil dari beberapa permintaan ke server dan kemudian melakukan "manipulasi sisi klien" data untuk menyematkan detail dalam array.
Jadi selain dari peringatan yang jelas bahwa karakteristik kinerja sama sekali tidak setara dengan server $lookup
, peringatan lainnya tentu saja bahwa "Dokumen luwak" dalam hasil sebenarnya bukanlah objek JavaScript biasa yang dapat dimanipulasi lebih lanjut.
Jadi untuk mengambil pendekatan ini, Anda perlu menambahkan .lean()
metode kueri sebelum eksekusi, untuk menginstruksikan luwak untuk mengembalikan "objek JavaScript biasa" alih-alih Document
jenis yang dilemparkan dengan metode skema yang melekat pada model. Tentu saja dengan mencatat bahwa data yang dihasilkan tidak lagi memiliki akses ke "metode instans" apa pun yang seharusnya dikaitkan dengan model terkait itu sendiri:
let venue = await Venue.findOne({ _id: id.id })
.populate({
path: 'reviews',
options: { sort: { createdAt: -1 } },
populate: [
{ path: 'comments', populate: [{ path: 'author' }] }
]
})
.lean();
Sekarang venue
adalah objek biasa, kita cukup memproses dan menyesuaikan sesuai kebutuhan:
venue.reviews = venue.reviews.map( r =>
({
...r,
comments: r.comments.map( c =>
({
...c,
author: {
...c.author,
isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
}
})
)
})
);
Jadi ini benar-benar hanya masalah bersepeda melalui masing-masing larik dalam hingga tingkat di mana Anda dapat melihat followers
larik di dalam author
rincian. Perbandingan kemudian dapat dilakukan terhadap ObjectId
nilai yang disimpan dalam array itu setelah pertama kali menggunakan .map()
untuk mengembalikan nilai "string" untuk perbandingan dengan req.user.id
yang juga berupa string (jika bukan, tambahkan juga .toString()
pada itu ), karena secara umum lebih mudah untuk membandingkan nilai-nilai ini dengan cara ini melalui kode JavaScript.
Sekali lagi meskipun saya perlu menekankan bahwa itu "terlihat sederhana" tetapi sebenarnya ini adalah hal yang benar-benar ingin Anda hindari untuk kinerja sistem, karena kueri tambahan dan transfer antara server dan klien menghabiskan banyak waktu pemrosesan dan bahkan karena permintaan yang berlebihan, hal ini menambah biaya nyata dalam transportasi antar penyedia hosting.
Ringkasan
Itu pada dasarnya adalah pendekatan yang dapat Anda ambil, selain "menggulung sendiri" di mana Anda benar-benar melakukan "beberapa kueri" ke database sendiri alih-alih menggunakan pembantu yang .populate()
adalah.
Dengan menggunakan output populate, Anda dapat dengan mudah memanipulasi data dalam hasil seperti struktur data lainnya, selama Anda menerapkan .lean()
ke kueri untuk mengonversi atau mengekstrak data objek biasa dari dokumen luwak yang dikembalikan.
Sementara pendekatan agregat terlihat jauh lebih terlibat, ada "banyak" lebih banyak keuntungan untuk melakukan pekerjaan ini di server. Kumpulan hasil yang lebih besar dapat diurutkan, perhitungan dapat dilakukan untuk pemfilteran lebih lanjut, dan tentu saja Anda mendapatkan "respon tunggal" ke "satu permintaan" dibuat ke server, semuanya tanpa biaya tambahan.
Benar-benar dapat diperdebatkan bahwa saluran pipa itu sendiri dapat dengan mudah dibangun berdasarkan atribut yang sudah disimpan pada skema. Jadi, menulis metode Anda sendiri untuk melakukan "konstruksi" ini berdasarkan skema terlampir seharusnya tidak terlalu sulit.
Dalam jangka panjang tentunya $lookup
adalah solusi yang lebih baik, tetapi Anda mungkin perlu melakukan sedikit lebih banyak pekerjaan ke dalam pengkodean awal, jika tentu saja Anda tidak hanya menyalin dari apa yang tercantum di sini;)