Er zijn natuurlijk een aantal benaderingen, afhankelijk van uw beschikbare MongoDB-versie. Deze variëren van verschillende toepassingen van $lookup
tot het inschakelen van objectmanipulatie op de .populate()
resultaat via .lean()
.
Ik vraag u wel om de secties aandachtig te lezen en zich ervan bewust te zijn dat bij het overwegen van uw implementatieoplossing misschien niet alles is wat het lijkt.
MongoDB 3.6, "geneste" $lookup
Met MongoDB 3.6 de $lookup
operator krijgt de extra mogelijkheid om een pipeline
op te nemen expressie in plaats van simpelweg een "lokale" naar "buitenlandse" sleutelwaarde toe te voegen, wat dit betekent is dat u in wezen elke $lookup
kunt doen als "geneste" binnen deze pijplijnuitdrukkingen
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"
}},
])
Dit kan echt behoorlijk krachtig zijn, zoals je ziet vanuit het perspectief van de originele pijplijn, het weet eigenlijk alleen over het toevoegen van inhoud aan de "reviews"
array en vervolgens ziet elke volgende "geneste" pijplijnexpressie ook alleen de "innerlijke" elementen van de join.
Het is krachtig en in sommige opzichten is het misschien een beetje duidelijker omdat alle veldpaden relatief zijn ten opzichte van het nestniveau, maar het begint wel dat inspringen in de BSON-structuur kruipt, en je moet je er wel van bewust zijn of je overeenkomt met arrays of enkelvoudige waarden bij het doorlopen van de structuur.
Merk op dat we hier ook dingen kunnen doen zoals "de eigenschap auteur afvlakken" zoals te zien is in de "comments"
array-items. Alle $lookup
doeluitvoer kan een "array" zijn, maar binnen een "subpijplijn" kunnen we die array met één element hervormen tot slechts een enkele waarde.
Standaard MongoDB $lookup
Nog steeds de "join on the server" behouden, je kunt het echt doen met $lookup
, maar er is slechts een tussentijdse verwerking voor nodig. Dit is de al lang bestaande benadering bij het deconstrueren van een array met $unwind
en het gebruik van $group
stadia om arrays opnieuw op te bouwen:
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"
}
}
}}
])
Dit is echt niet zo ontmoedigend als je in eerste instantie zou denken en volgt een eenvoudig patroon van $lookup
en $unwind
naarmate je door elke array vordert.
De "author"
detail is natuurlijk enkelvoud, dus als dat eenmaal "afgewikkeld" is, wil je het gewoon zo laten, de veldtoevoeging maken en het proces van "terugrollen" in de arrays starten.
Er zijn slechts twee niveaus om terug te reconstrueren naar de originele Venue
document, dus het eerste detailniveau is door Review
om de "comments"
opnieuw te maken reeks. Het enige wat je hoeft te doen is $push
het pad van "$reviews.comments"
om deze te verzamelen, en zolang de "$reviews._id"
veld is in de "groepering _id" de enige andere dingen die je moet bewaren zijn alle andere velden. U kunt deze allemaal in de _id
. plaatsen ook, of je kunt $first
. gebruiken .
Als dat klaar is, is er nog maar één $group
podium om terug te gaan naar Venue
zelf. Deze keer is de groeperingssleutel "$_id"
natuurlijk, met alle eigenschappen van de locatie zelf met behulp van $first
en de resterende "$review"
details gaan terug naar een array met $push
. Natuurlijk de "$comments"
uitvoer van de vorige $group
wordt de "review.comments"
pad.
Werken aan een enkel document en zijn relaties, is zo erg nog niet. De $unwind
pijpleidingbeheerder kan in het algemeen een prestatieprobleem zijn, maar in de context van dit gebruik zou het niet zo'n grote impact moeten hebben.
Aangezien de gegevens nog steeds "op de server worden samengevoegd", is er nog veel minder verkeer dan het andere overgebleven alternatief.
JavaScript-manipulatie
Natuurlijk is het andere geval hier dat in plaats van gegevens op de server zelf te wijzigen, u het resultaat manipuleert. In de meeste gevallen zou ik voorstander zijn van deze benadering, aangezien eventuele "toevoegingen" aan de gegevens waarschijnlijk het beste op de klant kunnen worden afgehandeld.
Het probleem natuurlijk met het gebruik van populate()
is dat terwijl het 'eruit kan zien' een veel eenvoudiger proces, het is in feite NIET EEN JOIN hoe dan ook. Alle populate()
eigenlijk doet is "verbergen" het onderliggende proces van het indienen van meerdere query's naar de database en wacht vervolgens op de resultaten via asynchrone afhandeling.
Dus de "uiterlijk" van een join is eigenlijk het resultaat van meerdere verzoeken aan de server en vervolgens "client-side manipulatie" van de gegevens om de details in arrays in te bedden.
Dus afgezien van die duidelijke waarschuwing dat de prestatiekenmerken lang niet in de buurt komen van een server $lookup
, het andere voorbehoud is natuurlijk dat de "mongoose-documenten" in het resultaat eigenlijk geen gewone JavaScript-objecten zijn die verder kunnen worden gemanipuleerd.
Dus om deze benadering te volgen, moet u de .lean()
. toevoegen methode toe aan de query vóór uitvoering, om mangoest te instrueren om "plain JavaScript-objecten" te retourneren in plaats van Document
typen die worden gegoten met schemamethoden die aan het model zijn gekoppeld. Natuurlijk opmerkend dat de resulterende gegevens niet langer toegang hebben tot "instantiemethoden" die anders zouden worden geassocieerd met de gerelateerde modellen zelf:
let venue = await Venue.findOne({ _id: id.id })
.populate({
path: 'reviews',
options: { sort: { createdAt: -1 } },
populate: [
{ path: 'comments', populate: [{ path: 'author' }] }
]
})
.lean();
Nu venue
is een eenvoudig object, we kunnen het eenvoudig verwerken en aanpassen als dat nodig is:
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
}
})
)
})
);
Het is dus eigenlijk gewoon een kwestie van door elk van de binnenste arrays heen fietsen tot het niveau waar je de followers
kunt zien. array binnen de author
details. De vergelijking kan dan worden gemaakt met de ObjectId
waarden die in die array zijn opgeslagen na het eerste gebruik van .map()
om de "string"-waarden te retourneren voor vergelijking met de req.user.id
wat ook een string is (als dat niet het geval is, voeg dan ook .toString()
toe op that ), omdat het over het algemeen gemakkelijker is om deze waarden op deze manier te vergelijken via JavaScript-code.
Nogmaals, ik moet benadrukken dat het "er eenvoudig uitziet", maar het is in feite het soort dingen dat je echt wilt vermijden voor systeemprestaties, omdat die extra vragen en de overdracht tussen de server en de client veel tijd van verwerking kosten en zelfs vanwege de overhead van het verzoek, leidt dit tot echte transportkosten tussen hostingproviders.
Samenvatting
Dat zijn in feite uw benaderingen die u kunt volgen, in plaats van "uw eigen weg te gaan", waarbij u daadwerkelijk de "meerdere zoekopdrachten" uitvoert zelf naar de database in plaats van de helper te gebruiken die .populate()
is.
Met behulp van de vuluitvoer kunt u de gegevens in het resultaat eenvoudig manipuleren, net als elke andere gegevensstructuur, zolang u .lean()
toepast. aan de query om de platte objectgegevens uit de geretourneerde mangoestdocumenten te converteren of anderszins te extraheren.
Hoewel de geaggregeerde benaderingen veel ingewikkelder lijken, zijn er "veel" meer voordelen om dit werk op de server te doen. Grotere resultatensets kunnen worden gesorteerd, berekeningen kunnen worden gedaan voor verdere filtering en natuurlijk krijg je een "single response" naar een "enkel verzoek" gemaakt naar de server, allemaal zonder extra overhead.
Het is volledig betwistbaar dat de pijplijnen zelf eenvoudig kunnen worden geconstrueerd op basis van kenmerken die al in het schema zijn opgeslagen. Dus het schrijven van uw eigen methode om deze "constructie" uit te voeren op basis van het bijgevoegde schema zou niet al te moeilijk moeten zijn.
Op de langere termijn natuurlijk $lookup
is de betere oplossing, maar je zult waarschijnlijk wat meer werk in de initiële codering moeten steken, als je natuurlijk niet gewoon kopieert van wat hier wordt vermeld;)