Sie können dies auf verschiedene Arten angehen. Sie variieren natürlich je nach Ansatz und Leistung, und ich denke, es gibt einige größere Überlegungen, die Sie bei Ihrem Design anstellen müssen. Am bemerkenswertesten ist hier der „Bedarf“ an „Überarbeitungsdaten“ im Nutzungsmuster Ihrer aktuellen Anwendung.
Abfrage über Aggregat
Was den wichtigsten Punkt betrifft, um das "letzte Element aus dem inneren Array" zu erhalten, dann sollten Sie wirklich einen .aggregate()
Operation, um dies zu tun:
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)
})
}
Da dies eine Aggregationsanweisung ist, können wir auch die „Joins“ auf dem „Server“ durchführen, anstatt zusätzliche Anfragen zu stellen (was .populate()
ist). tatsächlich hier ) durch Verwendung von $lookup
, ich nehme mir etwas Freiheit mit den tatsächlichen Sammlungsnamen, da Ihr Schema nicht in der Frage enthalten ist. Das ist in Ordnung, da Sie nicht wussten, dass Sie es tatsächlich so machen können.
Natürlich werden die "tatsächlichen" Sammlungsnamen vom Server benötigt, der kein Konzept des "anwendungsseitig" definierten Schemas hat. Es gibt Dinge, die Sie hier der Bequemlichkeit halber tun können, aber dazu später mehr.
Beachten Sie auch, dass je nach projectId
eigentlich kommt, dann im Gegensatz zu regulären Mongoose-Methoden wie .find()
das $match
erfordert tatsächlich ein "Casting" in eine ObjectId
wenn der Eingabewert tatsächlich ein "String" ist. Mongoose kann keine „Schematypen“ in einer Aggregationspipeline anwenden, daher müssen Sie dies möglicherweise selbst tun, insbesondere wenn projectId
kam von einem Anfrageparameter:
{ "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
Der grundlegende Teil hier ist, wo wir $map
um alle "uploaded_files"
zu durchlaufen Einträge und extrahieren Sie dann einfach das "Neueste" aus dem "Verlauf"
Array mit $arrayElemAt
unter Verwendung des "letzten" Index, der -1
ist .
Das sollte vernünftig sein, da es höchstwahrscheinlich ist, dass die "neueste Revision" tatsächlich der "letzte" Array-Eintrag ist. Wir könnten dies anpassen, um nach dem „größten“ zu suchen, indem wir $max
als Bedingung für $filter
. Diese Pipeline-Phase wird also zu:
{ "$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"
}
}
}
}},
Das ist mehr oder weniger dasselbe, außer dass wir den Vergleich mit dem machen $max
Wert und geben nur "eins" zurück Eintrag aus dem Array, wodurch der Index, der aus dem "gefilterten" Array zurückgegeben werden soll, die "erste" Position oder 0
ist index.
Wie bei anderen allgemeinen Techniken zur Verwendung von $lookup
anstelle von .populate()
, siehe meinen Eintrag zu "Querying after populate in Mongoose"
was ein bisschen mehr über Dinge spricht, die bei diesem Ansatz optimiert werden können.
Abfrage über Auffüllen
Natürlich können wir (wenn auch nicht so effizient) die gleiche Art von Operation mit .populate()
durchführen Aufrufe und Bearbeiten der resultierenden Arrays:
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)
Wobei Sie natürlich "alle" Elemente aus "history"
zurückgeben , aber wir wenden einfach eine .map an ()
zum Aufrufen von .slice()
auf diesen Elementen, um wieder das letzte Array-Element für jedes zu erhalten.
Etwas mehr Overhead, da der gesamte Verlauf zurückgegeben wird, und .populate()
Aufrufe sind zusätzliche Anforderungen, aber es werden die gleichen Endergebnisse erzielt.
Ein Designpunkt
Das Hauptproblem, das ich hier sehe, ist jedoch, dass Sie sogar ein "Verlaufs" -Array innerhalb des Inhalts haben. Dies ist nicht wirklich eine gute Idee, da Sie Dinge wie oben tun müssen, um nur den relevanten Artikel zurückzugeben, den Sie möchten.
Als "Designpunkt" würde ich dies also nicht tun. Aber stattdessen würde ich in allen Fällen die Historie von den Items "trennen". Um bei "eingebetteten" Dokumenten zu bleiben, würde ich den "Verlauf" in einem separaten Array aufbewahren und nur die "neueste" Revision mit dem tatsächlichen Inhalt behalten:
{
"_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
}
}
Sie können dies einfach beibehalten, indem Sie $set
festlegen
den entsprechenden Eintrag und verwenden Sie $push
auf die "Geschichte" in einem Vorgang:
.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
}
}
}
)
Wenn das Array getrennt ist, können Sie einfach abfragen und immer das Neueste abrufen und den "Verlauf" verwerfen, bis Sie diese Anfrage tatsächlich stellen möchten:
Project.findOne({ "project_id": projectId })
.select('-file_history') // The '-' here removes the field from results
.populate(populateQuery)
Im Allgemeinen würde ich mich jedoch überhaupt nicht mit der "Revisionsnummer" beschäftigen. Wenn Sie viel von der gleichen Struktur beibehalten, brauchen Sie es nicht wirklich, wenn Sie an ein Array "anhängen", da das "Neueste" immer das "Letzte" ist. Dies gilt auch für die Änderung der Struktur, wobei der "neueste" immer der letzte Eintrag für die angegebene hochgeladene Datei ist.
Der Versuch, einen solchen "künstlichen" Index zu pflegen, ist mit Problemen behaftet und ruiniert meistens jede Änderung von "atomaren" Operationen, wie in .update()
gezeigt Beispiel hier, da Sie einen "Zähler"-Wert kennen müssen, um die neueste Revisionsnummer bereitzustellen, und diesen daher irgendwo "lesen" müssen.