Можете да се справите с това по няколко различни начина. Разбира се, те варират в зависимост от подхода и производителността и мисля, че има някои по-големи съображения, които трябва да вземете във вашия дизайн. Най-вече тук е „нуждата“ от данни за „ревизии“ в модела на използване на вашето действително приложение.
Заявка чрез агрегат
Що се отнася до най-важната точка за получаване на „последния елемент от вътрешния масив“, тогава наистина трябва да използвате .aggregate()
операция за извършване на това:
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)
})
}
Тъй като това е оператор за агрегиране, където можем също така да правим „съединения“ на „сървъра“, вместо да правим допълнителни заявки (което е това, което .populate()
всъщност прави тук ), като използвате $lookup
, приемам известна свобода с действителните имена на колекции, тъй като вашата схема не е включена във въпроса. Всичко е наред, тъй като не сте осъзнавали, че всъщност можете да го направите по този начин.
Разбира се, "действителните" имена на колекции се изискват от сървъра, който няма концепция за дефинираната схема от "страна на приложението". Има неща, които можете да направите за удобство тук, но повече за това по-късно.
Трябва също да имате предвид, че в зависимост от това къде projectId
всъщност идва от, тогава за разлика от обикновените мангустови методи като .find()
$match
ще изисква действително "кастинг" към ObjectId
ако входната стойност всъщност е "низ". Mongoose не може да прилага „типове схеми“ в тръбопровод за агрегиране, така че може да се наложи да направите това сами, особено ако projectId
идва от параметър на заявка:
{ "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
Основната част тук е мястото, където използваме $map
за итерация през всички "uploaded_files"
записи и след това просто извлечете „най-новите“ от „history“
масив с $arrayElemAt
използвайки "последния" индекс, който е -1
.
Това би трябвало да е разумно, тъй като е най-вероятно "най-скорошната ревизия" всъщност да е "последният" запис в масива. Можем да адаптираме това, за да търсим „най-големия“, като приложим $max
като условие за $filter
. Така този етап на тръбопровод става:
{ "$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"
}
}
}
}},
Което е повече или по-малко същото, освен че правим сравнението с $max
стойност и връща само "one" запис от масива, който прави индекса да се върне от "филтрирания" масив на "първата" позиция или 0
индекс.
Що се отнася до други общи техники за използване на $lookup
на мястото на .populate()
, вижте моя запис на "Извършване на заявки след попълване в Mongoose"
който говори малко повече за нещата, които могат да бъдат оптимизирани при прилагането на този подход.
Запитване чрез попълване
Също така, разбира се, можем да направим (макар и не толкова ефективно) същия вид операция, използвайки .populate()
извиквания и манипулиране на получените масиви:
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)
Където, разбира се, всъщност връщате "всички" елементи от "history"
, но ние просто прилагаме .map ()
за извикване на .slice()код>
върху тези елементи, за да получите отново последния елемент от масива за всеки.
Малко повече разходи, тъй като се връща цялата история и .populate()
обажданията са допълнителни заявки, но се получават същите крайни резултати.
Точка на дизайна
Основният проблем, който виждам тук обаче, е, че дори имате масив "история" в съдържанието. Това всъщност не е страхотна идея, тъй като трябва да направите неща като по-горе, за да върнете само съответния артикул, който искате.
Така че като „точка на дизайна“ не бих направил това. Но вместо това бих "отделил" историята от елементите във всички случаи. Запазвайки „вградените“ документи, бих запазил „историята“ в отделен масив и бих запазил само „най-новата“ версия с действителното съдържание:
{
"_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
}
}
Можете да поддържате това просто като зададете $set
съответния запис и използване на $push
върху "историята" в една операция:
.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
}
}
}
)
След като масивът е разделен, тогава можете просто да правите заявки и винаги да получавате най-новите и да изхвърляте „историята“ до момента, в който наистина искате да направите тази заявка:
Project.findOne({ "project_id": projectId })
.select('-file_history') // The '-' here removes the field from results
.populate(populateQuery)
Като общ случай обаче аз просто изобщо не бих се занимавал с номера на "ревизацията". Запазването на голяма част от същата структура не ви е нужно наистина, когато "добавяте" към масив, тъй като "последното" винаги е "последно". Това важи и за промяна на структурата, където отново „най-новият“ винаги ще бъде последният запис за дадения качен файл.
Опитът да се поддържа такъв "изкуствен" индекс е изпълнен с проблеми и най-вече съсипва всяка промяна на "атомарните" операции, както е показано в .update()
пример тук, тъй като трябва да знаете стойност на "брояч", за да предоставите последния номер на ревизия, и следователно трябва да "прочетете" това отнякъде.