sql >> Databasteknik >  >> NoSQL >> MongoDB

$lookup flera nivåer utan $unwind?

Det finns naturligtvis ett par tillvägagångssätt beroende på din tillgängliga MongoDB-version. Dessa varierar från olika användningsområden för $lookup fram till att möjliggöra objektmanipulation på .populate() resultat via .lean() .

Jag ber dig att läsa avsnitten noggrant och vara medveten om att allt kanske inte är som det verkar när du överväger din implementeringslösning.

MongoDB 3.6, "kapslade" $lookup

Med MongoDB 3.6 är $lookup operatören får ytterligare möjlighet att inkludera en pipeline uttryck i motsats till att bara sammanfoga ett "lokalt" till "utländskt" nyckelvärde, vad detta betyder är att du i princip kan göra varje $lookup som "kapslade" i dessa pipeline-uttryck

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

Detta kan verkligen vara ganska kraftfullt, som du ser från den ursprungliga pipelinens perspektiv, det vet egentligen bara om att lägga till innehåll till "reviews" array och sedan varje efterföljande "kapslade" pipelineuttryck ser också bara sina "inre" element från sammanfogningen.

Den är kraftfull och i vissa avseenden kan den vara lite tydligare eftersom alla fältvägar är relativa till kapslingsnivån, men den börjar krypa in i BSON-strukturen, och du måste vara medveten om om du matchar arrayer. eller singularvärden för att korsa strukturen.

Observera att vi också kan göra saker här som att "platta ut författarens egendom" som visas i "comments" arrayposter. Alla $lookup målutgång kan vara en "array", men inom en "sub-pipeline" kan vi omforma den enda elementarrayen till ett enda värde.

Standard MongoDB $lookup

Om du fortfarande håller "join på servern" kan du faktiskt göra det med $lookup , men det tar bara mellanliggande bearbetning. Detta är det långvariga tillvägagångssättet med att dekonstruera en array med $unwind och använda $group steg för att bygga om arrayer:

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

Detta är verkligen inte så skrämmande som du kanske tror först och följer ett enkelt mönster av $lookup och $unwind när du går igenom varje array.

"author" detalj är naturligtvis singularis, så när det väl är "avlindat" vill du helt enkelt lämna det så, göra fälttillägget och starta processen att "rulla tillbaka" in i arrayerna.

Det finns bara två nivåer för att rekonstruera tillbaka till den ursprungliga Venue dokument, så den första detaljnivån är genom Review för att bygga om "comments" array. Allt du behöver är att $push sökvägen till "$reviews.comments" för att samla in dessa, och så länge som "$reviews._id" fältet är i "grouping _id" de enda andra sakerna du behöver behålla är alla andra fält. Du kan lägga in alla dessa i _id också, eller så kan du använda $first .

Med det gjort finns det bara en $group till steg för att komma tillbaka till Venue sig. Den här gången är grupperingsnyckeln "$_id" naturligtvis, med alla egenskaper för själva lokalen med $first och den återstående "$review" detaljer går tillbaka till en array med $push . Naturligtvis "$comments" utdata från föregående $group blir "review.comments" sökväg.

Att arbeta på ett enda dokument och dess relationer, det här är egentligen inte så illa. $unwind pipeline operatör kan allmänt vara ett prestandaproblem, men i samband med denna användning borde det egentligen inte ha så stor inverkan.

Eftersom data fortfarande "ansluts på servern" finns det fortfarande mycket mindre trafik än det andra återstående alternativet.

JavaScript-manipulation

Naturligtvis är det andra fallet här att istället för att ändra data på själva servern, manipulerar du faktiskt resultatet. I de flesta fall jag skulle vara för detta tillvägagångssätt eftersom alla "tillägg" till data förmodligen hanteras bäst på klienten.

Problemet såklart med att använda populate() är det även om det kan 'se ut som' en mycket mer förenklad process, det är faktiskt INTE EN JOIN på något sätt. Alla populate() faktiskt gör är "gömma" den underliggande processen för att skicka in flera förfrågningar till databasen, och väntar sedan på resultaten genom asynkron hantering.

Alltså "utseendet" av en anslutning är faktiskt resultatet av flera förfrågningar till servern och sedan "manipulation på klientsidan" av data för att bädda in detaljerna i arrayer.

Så bortsett från den tydliga varningen att prestandaegenskaperna inte är i närheten av att vara i nivå med en server $lookup , den andra varningen är naturligtvis att "mangosdokumenten" i resultatet faktiskt inte är vanliga JavaScript-objekt som kan manipuleras ytterligare.

Så för att använda detta tillvägagångssätt måste du lägga till .lean() metod till frågan före exekvering, för att instruera mongoose att returnera "vanliga JavaScript-objekt" istället för Document typer som gjuts med schemametoder kopplade till modellen. Notera naturligtvis att den resulterande datan inte längre har tillgång till några "instansmetoder" som annars skulle vara associerade med själva de relaterade modellerna:

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

Nu venue är ett vanligt objekt kan vi helt enkelt bearbeta och justera 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 är egentligen bara en fråga om att cykla genom var och en av de inre arrayerna ner till den nivå där du kan se followers array inom author detaljer. Jämförelsen kan sedan göras mot ObjectId värden lagrade i den arrayen efter att ha använt .map() för att returnera "sträng"-värdena för jämförelse med req.user.id som också är en sträng (om den inte är det, lägg till .toString() på det ), eftersom det i allmänhet är lättare att jämföra dessa värden på detta sätt via JavaScript-kod.

Återigen måste jag betona att det "ser enkelt ut" men det är faktiskt den typ av sak du verkligen vill undvika för systemprestanda, eftersom de ytterligare frågorna och överföringen mellan servern och klienten kostar mycket i bearbetningstiden och till och med på grund av förfrågningskostnaderna ökar detta till verkliga kostnader för transport mellan värdleverantörer.

Sammanfattning

Det är i princip dina tillvägagångssätt du kan ta, förutom att "rulla dina egna" där du faktiskt utför "flera frågor" till databasen själv istället för att använda hjälpen som .populate() är.

Genom att använda populate-utgången kan du sedan helt enkelt manipulera data i resultat precis som vilken annan datastruktur som helst, så länge du använder .lean() till frågan för att konvertera eller på annat sätt extrahera de vanliga objektdata från de returnerade mongoosedokumenten.

Även om de samlade tillvägagångssätten ser mycket mer involverade ut, finns det "många" fler fördelar med att utföra detta arbete på servern. Större resultatuppsättningar kan sorteras, beräkningar kan göras för ytterligare filtrering och naturligtvis får du ett "single response" till en "single request" görs till servern, allt utan extra omkostnader.

Det är helt tveksamt att själva pipelinesna helt enkelt skulle kunna konstrueras baserat på attribut som redan finns lagrade i schemat. Så att skriva din egen metod för att utföra denna "konstruktion" baserat på det bifogade schemat borde inte vara alltför svårt.

På längre sikt förstås $lookup är den bättre lösningen, men du kommer förmodligen behöva lägga lite mer arbete på den initiala kodningen, om du så klart inte bara kopierar från det som listas här;)




  1. Hur utför man en massuppdatering av dokument i MongoDB med Java?

  2. Kollisionssannolikhet för ObjectId vs UUID i ett stort distribuerat system

  3. Max försök har överskridits Undantagskö laravel

  4. Hitta dubbletter av värden i en MongoDB-array