Du kan tackla detta på ett par olika sätt. De varierar givetvis beroende på tillvägagångssätt och prestanda, och jag tror att det finns några större överväganden du måste göra för din design. Mest anmärkningsvärt här är "behovet" av "revisioner" data i användningsmönstret för din faktiska applikation.
Fråga via aggregat
När det gäller den främsta punkten med att hämta det "sista elementet från den inre arrayen", så borde du verkligen använda en .aggregate()
operation för att göra detta:
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)
})
}
Eftersom detta är en aggregeringssats där vi också kan göra "joins" på "servern" i motsats till att göra ytterligare förfrågningar (vilket är vad .populate()
faktiskt gör här ) genom att använda $lookup
, Jag tar mig lite frihet med de faktiska samlingsnamnen eftersom ditt schema inte ingår i frågan. Det är okej, eftersom du inte insåg att du faktiskt kunde göra det på det här sättet.
Naturligtvis krävs de "faktiska" samlingsnamnen av servern, som inte har något koncept för det "applikationssida" definierade schemat. Det finns saker du kan göra för bekvämlighet här, men mer om det senare.
Du bör också notera att beroende på var projectId
faktiskt kommer från, då till skillnad från vanliga mongoose-metoder som .find()
$match
kommer att kräva faktiskt "casting" till ett ObjectId
om inmatningsvärdet i själva verket är en "sträng". Mongoose kan inte tillämpa "schematyper" i en aggregeringspipeline, så du kan behöva göra detta själv, särskilt om projectId
kom från en begäran parameter:
{ "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
Den grundläggande delen här är där vi använder $map
att iterera genom alla "uppladdade_filer"
poster och sedan helt enkelt extrahera det "senaste" från "historik"
array med $arrayElemAt
med det "sista" indexet, som är -1
.
Det borde vara rimligt eftersom det är mest troligt att den "senaste revisionen" faktiskt är den "sista" arrayposten. Vi skulle kunna anpassa detta för att leta efter det "största", genom att använda $max
som ett villkor för att $filter
. Så det pipelinestadiet blir:
{ "$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"
}
}
}
}},
Vilket är mer eller mindre samma sak, förutom att vi gör jämförelsen med $max
värde och returnerar endast "ett" inmatning från arrayen gör att indexet returnerar från den "filtrerade" arrayen till den "första" positionen, eller 0
index.
När det gäller andra allmänna tekniker för att använda $lookup
i stället för .populate()
, se mitt inlägg om "Querying after populate in Mongoose"
som talar lite mer om saker som kan optimeras när man tar detta tillvägagångssätt.
Fråga via fylla
Naturligtvis kan vi också göra (även om det inte är lika effektivt) samma typ av operation med .populate()
anrop och manipulering av de resulterande arrayerna:
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)
Där du naturligtvis returnerar "alla" objekt från "historik"
, men vi använder helt enkelt en .map ()
för att anropa .slice()
på dessa element för att återigen få det sista arrayelementet för varje.
Lite mer overhead eftersom all historik returneras, och .populate()
samtal är ytterligare förfrågningar, men det ger samma slutresultat.
En designpunkt
Det största problemet jag ser här är dock att du till och med har en "historik"-array i innehållet. Detta är egentligen ingen bra idé eftersom du behöver göra saker som ovan för att bara returnera den relevanta artikeln du vill ha.
Så som en "point of design" skulle jag inte göra det här. Men istället skulle jag "separera" historien från föremålen i alla fall. Med "inbäddade" dokument skulle jag behålla "historiken" i en separat array och bara behålla den "senaste" versionen med det faktiska innehållet:
{
"_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
}
}
Du kan underhålla detta helt enkelt genom att ställa in $set
relevant post och använda $push
på "historiken" i en operation:
.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
}
}
}
)
Med arrayen separerad kan du helt enkelt fråga och alltid få det senaste, och kassera "historiken" tills du faktiskt vill göra den begäran:
Project.findOne({ "project_id": projectId })
.select('-file_history') // The '-' here removes the field from results
.populate(populateQuery)
Som ett allmänt fall skulle jag helt enkelt inte bry mig om "revisions"-numret alls. Att behålla mycket av samma struktur behöver du inte riktigt när du "lägger" till en array eftersom det "senaste" alltid är det "sista". Detta gäller även för att ändra strukturen, där återigen den "senaste" alltid kommer att vara den sista posten för den givna uppladdade filen.
Att försöka upprätthålla ett sådant "artificiellt" index är fyllt med problem och förstör mestadels alla förändringar av "atomära" operationer som visas i .update()
exempel här, eftersom du behöver känna till ett "räknevärde" för att kunna ange det senaste versionsnumret och därför behöver "läsa" det någonstans ifrån.