sql >> Database teknologi >  >> NoSQL >> MongoDB

$lookup flere niveauer uden $unwind?

Der er selvfølgelig et par tilgange afhængigt af din tilgængelige MongoDB-version. Disse varierer fra forskellige anvendelser af $lookup til at aktivere objektmanipulation på .populate() resultat via .lean() .

Jeg beder dig læse afsnittene omhyggeligt og være opmærksom på, at alt måske ikke er, som det ser ud, når du overvejer din implementeringsløsning.

MongoDB 3.6, "indlejret" $lookup

Med MongoDB 3.6 er $lookup operatør får yderligere mulighed for at inkludere en pipeline udtryk i modsætning til blot at forbinde en "lokal" til "fremmed" nøgleværdi, hvad dette betyder er, at du i det væsentlige kan udføre hver $lookup som "indlejret" i disse pipeline-udtryk

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"
  }},
 ])

Dette kan virkelig være ret kraftfuldt, som du ser fra den originale pipelines perspektiv, ved det egentlig kun om at tilføje indhold til "reviews" array og derefter hvert efterfølgende "indlejrede" pipelineudtryk ser også kun sine "indre" elementer fra joinforbindelsen.

Det er kraftfuldt og i nogle henseender kan det være en smule tydeligere, da alle feltstier er i forhold til redeniveauet, men det starter det fordybningskryb i BSON-strukturen, og du skal være opmærksom på, om du matcher til arrays. eller enkeltværdier ved at krydse strukturen.

Bemærk, at vi også kan gøre ting her som "udfladning af forfatteregenskaben", som det ses i "comments" matrixindgange. Alle $lookup måloutput kan være et "array", men inden for en "sub-pipeline" kan vi omforme det enkelte element-array til kun en enkelt værdi.

Standard MongoDB $lookup

Hvis du stadig beholder "join på serveren", kan du faktisk gøre det med $lookup , men det kræver kun mellembehandling. Dette er den langvarige tilgang med at dekonstruere et array med $unwind og bruge $group trin for at genopbygge arrays:

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"
      }
    }
  }}
])

Dette er virkelig ikke så skræmmende, som du måske tror i starten og følger et simpelt mønster af $lookup og $unwind efterhånden som du går gennem hvert array.

"author" detaljen er selvfølgelig ental, så når først det er "viklet ud", vil du bare lade det være sådan, foretage feltadditionen og starte processen med at "rulle tilbage" ind i arrays.

Der er kun to niveauer for at rekonstruere tilbage til det oprindelige Venue dokument, så det første detaljeniveau er ved Review for at genopbygge "comments" array. Alt du behøver er at $push stien til "$reviews.comments" for at indsamle disse, og så længe "$reviews._id" feltet er i "grouping _id" de eneste andre ting du skal beholde er alle de andre felter. Du kan indsætte alle disse i _id også, eller du kan bruge $first .

Når det er gjort, er der kun én mere $group trin for at komme tilbage til Venue sig selv. Denne gang er grupperingsnøglen "$_id" selvfølgelig med alle egenskaber på selve spillestedet ved at bruge $first og den resterende "$review" detaljer går tilbage til et array med $push . Selvfølgelig "$comments" output fra den forrige $group bliver "review.comments" sti.

At arbejde på et enkelt dokument og dets relationer, det er egentlig ikke så slemt. $unwind pipeline operatør kan generelt være et præstationsproblem, men i forbindelse med denne brug burde det egentlig ikke have den store indflydelse.

Da dataene stadig "tilsluttes på serveren", er der stadig langt mindre trafik end det øvrige resterende alternativ.

JavaScript-manipulation

Selvfølgelig er det andet tilfælde her, at i stedet for at ændre data på selve serveren, manipulerer du faktisk resultatet. I de fleste tilfælde vil jeg gå ind for denne tilgang, da eventuelle "tilføjelser" til dataene nok bedst håndteres på klienten.

Problemet selvfølgelig med at bruge populate() er det mens det kan 'ligne' en meget mere forenklet proces, det er faktisk IKKE EN JOIN på nogen måde. Alle populate() faktisk gør, er "skjul" den underliggende proces med at indsende flere forespørgsler til databasen og afventer derefter resultaterne gennem asynkron håndtering.

"udseendet" af en joinforbindelse er faktisk resultatet af flere anmodninger til serveren og derefter foretager "klientsidemanipulation" af dataene for at indlejre detaljerne i arrays.

Så bortset fra den klare advarsel at ydeevneegenskaberne ikke er i nærheden af ​​at være på niveau med en server $lookup , den anden advarsel er naturligvis, at "mongoose-dokumenterne" i resultatet faktisk ikke er almindelige JavaScript-objekter, der er genstand for yderligere manipulation.

Så for at tage denne tilgang, skal du tilføje .lean() metode til forespørgslen før udførelse, for at instruere mongoose til at returnere "almindelige JavaScript-objekter" i stedet for Document typer som er støbt med skemametoder knyttet til modellen. Bemærk naturligvis, at de resulterende data ikke længere har adgang til nogen "instansmetoder", der ellers ville være forbundet med de relaterede modeller selv:

let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();

Nu venue er et almindeligt objekt, kan vi blot behandle og justere efter behov:

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
        }
      })
    )
  })
);

Så det er egentlig bare et spørgsmål om at cykle gennem hver af de indre arrays ned til det niveau, hvor du kan se followers array i author detaljer. Sammenligningen kan derefter foretages mod ObjectId værdier gemt i det array efter første brug af .map() for at returnere "streng"-værdierne til sammenligning med req.user.id som også er en streng (hvis den ikke er det, så tilføj også .toString() på det ), da det generelt er lettere at sammenligne disse værdier på denne måde via JavaScript-kode.

Igen skal jeg understrege, at det "ser simpelt ud", men det er faktisk den slags ting, du virkelig vil undgå for systemets ydeevne, da disse yderligere forespørgsler og overførslen mellem serveren og klienten koster meget i behandlingstiden og selv på grund af forespørgslens overhead stiger dette til reelle omkostninger ved transport mellem hostingudbydere.

Oversigt

Det er dybest set dine tilgange, du kan tage, bortset fra at "rulle dine egne", hvor du rent faktisk udfører "flere forespørgsler" til databasen selv i stedet for at bruge hjælperen, .populate() er.

Ved at bruge udfyldningsoutputtet kan du derefter blot manipulere dataene i resultatet ligesom enhver anden datastruktur, så længe du anvender .lean() til forespørgslen om at konvertere eller på anden måde udtrække de almindelige objektdata fra de returnerede mongoose-dokumenter.

Selvom de samlede tilgange ser langt mere involverede ud, er der "en masse" flere fordele ved at udføre dette arbejde på serveren. Større resultatsæt kan sorteres, der kan foretages beregninger for yderligere filtrering, og du får selvfølgelig et "enkelt svar" til en "enkelt anmodning" lavet til serveren, alt sammen uden yderligere overhead.

Det er fuldstændig diskutabelt, at selve rørledningerne simpelthen kunne konstrueres baseret på attributter, der allerede er gemt på skemaet. Så det burde ikke være for svært at skrive din egen metode til at udføre denne "konstruktion" baseret på det vedhæftede skema.

På længere sigt selvfølgelig $lookup er den bedre løsning, men du skal nok lægge lidt mere arbejde i den indledende kodning, hvis du selvfølgelig ikke bare kopierer fra det, der står her;)




  1. Apache HBase-regionopdeling og sammenlægning

  2. Hvordan bruger man uordnet bulk-indføring med Mongoskin?

  3. Hvordan opretter jeg en resque worker automatisk ved opstart?

  4. MongoDB ikke lig med