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

$cerca più livelli senza $unwind?

Ci sono un paio di approcci ovviamente a seconda della versione MongoDB disponibile. Questi variano a seconda dei diversi usi di $lookup fino all'abilitazione della manipolazione degli oggetti su .populate() risultato tramite .lean() .

Ti chiedo di leggere attentamente le sezioni e di essere consapevole del fatto che tutto potrebbe non essere come sembra quando consideri la tua soluzione di implementazione.

MongoDB 3.6, $lookup "nidificato"

Con MongoDB 3.6 il $lookup l'operatore ottiene la possibilità aggiuntiva di includere una pipeline espressione invece di unire semplicemente un valore chiave da "locale" a "estraneo", ciò significa che puoi essenzialmente eseguire ogni $lookup come "nidificato" all'interno di queste espressioni pipeline

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

Questo può essere davvero molto potente, come puoi vedere dal punto di vista della pipeline originale, sa solo come aggiungere contenuti alle "reviews" array e quindi ogni successiva espressione della pipeline "nidificata" vede sempre e solo i suoi elementi "interni" dal join.

È potente e per alcuni aspetti potrebbe essere un po' più chiaro poiché tutti i percorsi di campo sono relativi al livello di nidificazione, ma fa iniziare che l'indentazione si insinua nella struttura BSON e devi essere consapevole se stai abbinando agli array o valori singolari nell'attraversare la struttura.

Nota che possiamo anche fare cose come "appiattire la proprietà dell'autore" come si vede all'interno di "comments" voci dell'array. Tutti i $lookup l'output di destinazione può essere un "array", ma all'interno di una "sotto-pipeline" possiamo rimodellare quell'array di elementi singoli in un solo valore.

Ricerca $ MongoDB standard

Mantenendo il "join sul server" puoi effettivamente farlo con $lookup , ma richiede solo un'elaborazione intermedia. Questo è l'approccio di vecchia data con la decostruzione di un array con $unwind e l'utilizzo di $group fasi per ricostruire gli array:

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

Questo non è davvero così scoraggiante come potresti pensare all'inizio e segue un semplice schema di $lookup e $unwind man mano che avanzi in ogni array.

Il "author" il dettaglio ovviamente è singolare, quindi una volta "srotolato" vuoi semplicemente lasciarlo così, fare l'aggiunta del campo e iniziare il processo di "rollback" negli array.

Ce ne sono solo due livelli per ricostruire il Venue originale documento, quindi il primo livello di dettaglio è per Review per ricostruire i "comments" Vettore. Tutto quello che devi fare è $push il percorso di "$reviews.comments" per raccoglierli e purché il "$reviews._id" il campo è nel "raggruppamento _id" le uniche altre cose che devi mantenere sono tutti gli altri campi. Puoi metterli tutti nel _id oppure puoi usare $first .

Fatto ciò c'è solo un altro $group fase per tornare a Venue si. Questa volta la chiave di raggruppamento è "$_id" ovviamente, con tutte le proprietà della sede stessa utilizzando $first e il restante "$review" dettagli che tornano in un array con $push . Ovviamente il "$comments" output dal precedente $group diventa il "review.comments" percorso.

Lavorare su un unico documento e le sue relazioni, non è poi così male. Il $unwind l'operatore della pipeline può generalmente essere un problema di prestazioni, ma nel contesto di questo utilizzo non dovrebbe davvero avere un impatto così grande.

Poiché i dati vengono ancora "uniti sul server", c'è ancora molto meno traffico rispetto all'altra alternativa rimanente.

Manipolazione JavaScript

Naturalmente l'altro caso qui è che invece di modificare i dati sul server stesso, si manipola effettivamente il risultato. Nella maggior parte in casi Sarei favorevole a questo approccio poiché eventuali "aggiunte" ai dati sono probabilmente gestite al meglio sul client.

Il problema ovviamente con l'utilizzo di populate() è che mentre può 'sembrare' un processo molto più semplificato, infatti NON UN'UNIONE in ogni modo. Tutti populate() in realtà è "nascondi" il processo sottostante di invio di più query al database e quindi in attesa dei risultati tramite la gestione asincrona.

Quindi l'"aspetto" di un join è in realtà il risultato di più richieste al server e quindi di "manipolazione lato client" dei dati per incorporare i dettagli negli array.

Quindi, a parte questo chiaro avvertimento che le caratteristiche prestazionali non sono neanche lontanamente paragonabili a quelle di un server $lookup , l'altro avvertimento è ovviamente che i "documenti mangusta" nel risultato non sono in realtà semplici oggetti JavaScript soggetti a ulteriore manipolazione.

Quindi, per adottare questo approccio, devi aggiungere .lean() metodo alla query prima dell'esecuzione, per istruire mongoose a restituire "oggetti JavaScript semplici" invece di Document tipi che vengono cast con metodi schema allegati al modello. Notando ovviamente che i dati risultanti non hanno più accesso ad alcun "metodo di istanza" che altrimenti sarebbe associato ai relativi modelli stessi:

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

Ora venue è un oggetto semplice, possiamo semplicemente elaborare e regolare secondo necessità:

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

Quindi è davvero solo una questione di scorrere ciascuno degli array interni fino al livello in cui puoi vedere i followers array all'interno dell'author dettagli. Il confronto può quindi essere effettuato con ObjectId valori memorizzati in quell'array dopo aver usato per la prima volta .map() per restituire i valori "string" per il confronto con req.user.id che è anche una stringa (se non lo è, aggiungi anche .toString() su quello ), poiché è più facile in generale confrontare questi valori in questo modo tramite codice JavaScript.

Ancora una volta, però, devo sottolineare che "sembra semplice" ma in realtà è il tipo di cosa che vuoi davvero evitare per le prestazioni del sistema, poiché quelle query aggiuntive e il trasferimento tra il server e il client costano molto nel tempo di elaborazione e anche a causa delle spese generali della richiesta, ciò si aggiunge ai costi reali di trasporto tra i provider di hosting.

Riepilogo

Questi sono fondamentalmente i tuoi approcci che puoi adottare, a parte il "rolling your own" in cui esegui effettivamente le "query multiple" al database tu stesso invece di usare l'helper che .populate() è.

Usando l'output di popolamento, puoi semplicemente manipolare i dati nel risultato proprio come qualsiasi altra struttura di dati, purché applichi .lean() alla query per convertire o estrarre in altro modo i dati dell'oggetto semplice dai documenti mangusta restituiti.

Sebbene gli approcci aggregati sembrino molto più coinvolti, ce ne sono "molti" più vantaggi nel fare questo lavoro sul server. È possibile ordinare set di risultati più grandi, eseguire calcoli per filtrare ulteriormente e, naturalmente, ottenere una "risposta singola" a una "richiesta singola" fatto al server, il tutto senza alcun sovraccarico aggiuntivo.

È assolutamente discutibile che le pipeline stesse possano essere semplicemente costruite sulla base di attributi già archiviati nello schema. Quindi scrivere il tuo metodo per eseguire questa "costruzione" basata sullo schema allegato non dovrebbe essere troppo difficile.

A lungo termine ovviamente $lookup è la soluzione migliore, ma probabilmente dovrai dedicare un po' più di lavoro alla codifica iniziale, se ovviamente non ti limiti semplicemente a copiare da ciò che è elencato qui;)