Anda bisa mengatasi ini dengan beberapa cara berbeda. Mereka bervariasi dalam pendekatan dan kinerja tentu saja, dan saya pikir ada beberapa pertimbangan yang lebih besar yang perlu Anda buat untuk desain Anda. Yang paling menonjol di sini adalah "kebutuhan" untuk data "revisi" dalam pola penggunaan aplikasi Anda yang sebenarnya.
Kueri melalui agregat
Adapun poin terpenting untuk mendapatkan "elemen terakhir dari array dalam", maka Anda benar-benar harus menggunakan .aggregate()
operasi untuk melakukan ini:
function getProject(req,projectId) {
return new Promise((resolve,reject) => {
Project.aggregate([
{ "$match": { "project_id": projectId } },
{ "$addFields": {
"uploaded_files": {
"$map": {
"input": "$uploaded_files",
"as": "f",
"in": {
"latest": {
"$arrayElemAt": [
"$$f.history",
-1
]
},
"_id": "$$f._id",
"display_name": "$$f.display_name"
}
}
}
}},
{ "$lookup": {
"from": "owner_collection",
"localField": "owner",
"foreignField": "_id",
"as": "owner"
}},
{ "$unwind": "$uploaded_files" },
{ "$lookup": {
"from": "files_collection",
"localField": "uploaded_files.latest.file",
"foreignField": "_id",
"as": "uploaded_files.latest.file"
}},
{ "$group": {
"_id": "$_id",
"project_id": { "$first": "$project_id" },
"updated_at": { "$first": "$updated_at" },
"created_at": { "$first": "$created_at" },
"owner" : { "$first": { "$arrayElemAt": [ "$owner", 0 ] } },
"name": { "$first": "$name" },
"uploaded_files": {
"$push": {
"latest": { "$arrayElemAt": [ "$$uploaded_files", 0 ] },
"_id": "$$uploaded_files._id",
"display_name": "$$uploaded_files.display_name"
}
}
}}
])
.then(result => {
if (result.length === 0)
reject(new createError.NotFound(req.path));
resolve(result[0])
})
.catch(reject)
})
}
Karena ini adalah pernyataan agregasi di mana kita juga dapat melakukan "bergabung" pada "server" sebagai lawan membuat permintaan tambahan ( yaitu .populate()
sebenarnya di sini ) dengan menggunakan $lookup
, Saya mengambil sedikit kebebasan dengan nama koleksi yang sebenarnya karena skema Anda tidak termasuk dalam pertanyaan. Tidak apa-apa, karena Anda tidak menyadari bahwa Anda sebenarnya bisa melakukannya dengan cara ini.
Tentu saja nama koleksi "sebenarnya" diperlukan oleh server, yang tidak memiliki konsep skema yang ditentukan "sisi aplikasi". Ada beberapa hal yang dapat Anda lakukan untuk kenyamanan di sini, tetapi lebih lanjut tentang itu nanti.
Anda juga harus mencatat bahwa tergantung di mana projectId
sebenarnya berasal dari, maka tidak seperti metode luwak biasa seperti .find()
$match
akan benar-benar membutuhkan "casting" ke ObjectId
jika nilai input sebenarnya adalah "string". Luwak tidak dapat menerapkan "jenis skema" dalam pipa agregasi, jadi Anda mungkin perlu melakukannya sendiri, terutama jika projectId
berasal dari parameter permintaan:
{ "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
Bagian dasar di sini adalah tempat kita menggunakan $map
untuk mengulangi semua "uploaded_files"
entri, lalu cukup ekstrak "terbaru" dari "history"
array dengan $arrayElemAt
menggunakan indeks "terakhir", yaitu -1
.
Itu harus masuk akal karena kemungkinan besar "revisi terbaru" sebenarnya adalah entri array "terakhir". Kita dapat mengadaptasi ini untuk mencari yang "terbesar", dengan menerapkan $max
sebagai syarat untuk $filter
. Sehingga tahapan pipeline menjadi:
{ "$addFields": {
"uploaded_files": {
"$map": {
"input": "$uploaded_files",
"as": "f",
"in": {
"latest": {
"$arrayElemAt": [
{ "$filter": {
"input": "$$f.history.revision",
"as": "h",
"cond": {
"$eq": [
"$$h",
{ "$max": "$$f.history.revision" }
]
}
}},
0
]
},
"_id": "$$f._id",
"display_name": "$$f.display_name"
}
}
}
}},
Yang kurang lebih sama, kecuali kita melakukan perbandingan dengan $max
nilai, dan hanya mengembalikan "satu" entri dari larik membuat indeks kembali dari larik "terfilter" ke posisi "pertama", atau 0
indeks.
Adapun Teknik umum lainnya dalam menggunakan $lookup
sebagai pengganti .populate()
, lihat entri saya di "Permintaan setelah diisi dalam bahasa Mongoose"
yang berbicara lebih banyak tentang hal-hal yang dapat dioptimalkan saat mengambil pendekatan ini.
Kueri melalui isi
Juga tentu saja kita dapat melakukan ( meskipun tidak seefisien ) jenis operasi yang sama menggunakan .populate()
memanggil dan memanipulasi array yang dihasilkan:
Project.findOne({ "project_id": projectId })
.populate(populateQuery)
.lean()
.then(project => {
if (project === null)
reject(new createError.NotFound(req.path));
project.uploaded_files = project.uploaded_files.map( f => ({
latest: f.history.slice(-1)[0],
_id: f._id,
display_name: f.display_name
}));
resolve(project);
})
.catch(reject)
Di mana tentu saja Anda benar-benar mengembalikan "semua" item dari "history"
, tapi kami hanya menerapkan .map()
untuk memanggil .slice()
pada elemen tersebut untuk mendapatkan kembali elemen larik terakhir untuk masing-masing elemen tersebut.
Sedikit lebih banyak overhead karena semua riwayat dikembalikan, dan .populate()
panggilan adalah permintaan tambahan, tetapi mendapatkan hasil akhir yang sama.
Titik desain
Masalah utama yang saya lihat di sini adalah Anda bahkan memiliki array "riwayat" di dalam konten. Ini bukan ide yang bagus karena Anda perlu melakukan hal-hal seperti di atas agar hanya mengembalikan item yang relevan yang Anda inginkan.
Jadi sebagai "titik desain", saya tidak akan melakukan ini. Tetapi sebaliknya saya akan "memisahkan" sejarah dari item dalam semua kasus. Sesuai dengan dokumen "tertanam", saya akan menyimpan "riwayat" dalam larik terpisah, dan hanya menyimpan revisi "terbaru" dengan konten yang sebenarnya:
{
"_id" : ObjectId("5935a41f12f3fac949a5f925"),
"project_id" : 13,
"updated_at" : ISODate("2017-07-02T22:11:43.426Z"),
"created_at" : ISODate("2017-06-05T18:34:07.150Z"),
"owner" : ObjectId("591eea4439e1ce33b47e73c3"),
"name" : "Demo project",
"uploaded_files" : [
{
"latest" : {
{
"file" : ObjectId("59596f9fb6c89a031019bcae"),
"revision" : 1
}
},
"_id" : ObjectId("59596f9fb6c89a031019bcaf"),
"display_name" : "Example filename.txt"
}
]
"file_history": [
{
"_id": ObjectId("59596f9fb6c89a031019bcaf"),
"file": ObjectId("59596f9fb6c89a031019bcae"),
"revision": 0
},
{
"_id": ObjectId("59596f9fb6c89a031019bcaf"),
"file": ObjectId("59596f9fb6c89a031019bcae"),
"revision": 1
}
}
Anda dapat mempertahankan ini hanya dengan menyetel $set
entri yang relevan dan menggunakan $push
pada "sejarah" dalam satu operasi:
.update(
{ "project_id": projectId, "uploaded_files._id": fileId }
{
"$set": {
"uploaded_files.$.latest": {
"file": revisionId,
"revision": revisionNum
}
},
"$push": {
"file_history": {
"_id": fileId,
"file": revisionId,
"revision": revisionNum
}
}
}
)
Dengan array yang terpisah, maka Anda cukup melakukan kueri dan selalu mendapatkan yang terbaru, dan membuang "riwayat" sampai Anda benar-benar ingin membuat permintaan itu:
Project.findOne({ "project_id": projectId })
.select('-file_history') // The '-' here removes the field from results
.populate(populateQuery)
Sebagai kasus umum, saya tidak akan repot dengan nomor "revisi" sama sekali. Menjaga banyak struktur yang sama Anda tidak benar-benar membutuhkannya saat "menambahkan" ke array karena "terbaru" selalu yang "terakhir". Ini juga berlaku untuk mengubah struktur, di mana sekali lagi "terbaru" akan selalu menjadi entri terakhir untuk file yang diunggah.
Mencoba mempertahankan indeks "buatan" seperti itu penuh dengan masalah, dan sebagian besar merusak setiap perubahan operasi "atom" seperti yang ditunjukkan dalam .update()
contoh di sini, karena Anda perlu mengetahui nilai "penghitung" untuk memberikan nomor revisi terbaru, dan oleh karena itu perlu "membaca" itu dari suatu tempat.