MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

$lookup plusieurs niveaux sans $unwind ?

Il existe bien sûr deux approches en fonction de votre version MongoDB disponible. Celles-ci varient selon les différentes utilisations de $lookup jusqu'à l'activation de la manipulation d'objet sur le .populate() résultat via .lean() .

Je vous demande de lire attentivement les sections et d'être conscient que tout peut ne pas être comme il semble lorsque vous envisagez votre solution de mise en œuvre.

MongoDB 3.6, $recherche "imbriquée"

Avec MongoDB 3.6, le $lookup l'opérateur obtient la possibilité supplémentaire d'inclure un pipeline expression au lieu de simplement joindre une valeur de clé "locale" à "étrangère", cela signifie que vous pouvez essentiellement faire chaque $lookup comme "imbriqué" dans ces expressions de 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"
  }},
 ])

Cela peut être vraiment très puissant, comme vous le voyez du point de vue du pipeline d'origine, il ne sait vraiment que l'ajout de contenu aux "reviews" tableau, puis chaque expression de pipeline "imbriquée" suivante ne voit que ses éléments "internes" de la jointure.

Il est puissant et à certains égards, il peut être un peu plus clair car tous les chemins de champ sont relatifs au niveau d'imbrication, mais cela commence ce fluage d'indentation dans la structure BSON, et vous devez savoir si vous correspondez à des tableaux ou des valeurs singulières en traversant la structure.

Notez que nous pouvons également faire des choses ici comme "aplatir la propriété de l'auteur" comme on le voit dans les "comments" entrées de tableau. Tous les $lookup la sortie cible peut être un "tableau", mais dans un "sous-pipeline", nous pouvons remodeler ce tableau d'éléments unique en une seule valeur.

Recherche $ MongoDB standard

En gardant toujours la "joindre sur le serveur", vous pouvez le faire avec $lookup , mais cela nécessite juste un traitement intermédiaire. C'est l'approche de longue date avec la déconstruction d'un tableau avec $unwind et l'utilisation de $group étapes pour reconstruire les tableaux :

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

Ce n'est vraiment pas aussi intimidant que vous pourriez le penser au début et suit un modèle simple de $lookup et $unwind au fur et à mesure que vous progressez dans chaque tableau.

Le "author" le détail est bien sûr singulier, donc une fois que c'est "déroulé", vous voulez simplement le laisser ainsi, faire l'ajout de champ et commencer le processus de "retour en arrière" dans les tableaux.

Il n'y en a que deux niveaux pour reconstruire le Venue d'origine document, donc le premier niveau de détail est par Review pour reconstruire les "comments" déployer. Tout ce dont vous avez besoin est de $push le chemin de "$reviews.comments" afin de les collecter, et tant que le "$reviews._id" champ est dans le "grouping _id" les seules autres choses que vous devez conserver sont tous les autres champs. Vous pouvez mettre tout cela dans le _id également, ou vous pouvez utiliser $first .

Cela fait, il n'y a plus qu'un seul $group étape afin de revenir à Venue lui-même. Cette fois, la clé de regroupement est "$_id" bien sûr, avec toutes les propriétés du lieu lui-même en utilisant $first et le reste "$review" les détails remontent dans un tableau avec $push . Bien sûr le "$comments" sortie du $group précédent devient le "review.comments" chemin.

Travailler sur un seul document et ses relations, ce n'est vraiment pas si mal. Le $unwind l'opérateur de pipeline peut généralement être un problème de performances, mais dans le contexte de cette utilisation, cela ne devrait pas vraiment avoir un impact aussi important.

Étant donné que les données sont toujours "jointes sur le serveur", il y a toujours beaucoup moins de trafic que l'autre alternative restante.

Manipulation JavaScript

Bien sûr, l'autre cas ici est qu'au lieu de modifier les données sur le serveur lui-même, vous manipulez en fait le résultat. Dans la plupart cas, je serais en faveur de cette approche car tout "ajout" aux données est probablement mieux géré sur le client.

Le problème bien sûr avec l'utilisation de populate() est-ce que cela peut "ressembler" un processus beaucoup plus simplifié, ce n'est en fait PAS UNE JOIN de quelque manière que. Tout populate() fait en réalité "cacher" le processus sous-jacent de soumission de plusieurs requêtes à la base de données, puis en attente des résultats via la gestion asynchrone.

Donc l'"apparence" d'une jointure est en fait le résultat de plusieurs requêtes adressées au serveur, puis d'une "manipulation côté client" des données pour intégrer les détails dans des tableaux.

Donc, à part cet avertissement clair que les caractéristiques de performance sont loin d'être comparables à celles d'un serveur $lookup , l'autre mise en garde est bien sûr que les "documents mangouste" dans le résultat ne sont pas réellement des objets JavaScript simples sujets à d'autres manipulations.

Donc, pour adopter cette approche, vous devez ajouter le .lean() méthode à la requête avant l'exécution, afin de demander à mongoose de renvoyer des "objets JavaScript simples" au lieu de Document types qui sont convertis avec des méthodes de schéma attachées au modèle. En notant bien sûr que les données résultantes n'ont plus accès à aucune "méthode d'instance" qui serait autrement associée aux modèles associés eux-mêmes :

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

Maintenant venue est un objet simple, nous pouvons simplement le traiter et l'ajuster au besoin :

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

Il s'agit donc simplement de parcourir chacun des tableaux internes jusqu'au niveau où vous pouvez voir les followers tableau dans le author des détails. La comparaison peut alors être faite avec le ObjectId valeurs stockées dans ce tableau après la première utilisation de .map() pour renvoyer les valeurs "string" pour comparaison avec req.user.id qui est aussi une chaîne (si ce n'est pas le cas, ajoutez également .toString() sur ce ), car il est en général plus facile de comparer ces valeurs de cette manière via du code JavaScript.

Encore une fois, je dois souligner que cela "a l'air simple", mais c'est en fait le genre de chose que vous voulez vraiment éviter pour les performances du système, car ces requêtes supplémentaires et le transfert entre le serveur et le client coûtent cher en temps de traitement et même en raison des frais généraux de demande, cela s'ajoute à des coûts réels de transport entre les hébergeurs.

Résumé

Ce sont essentiellement vos approches que vous pouvez adopter, à moins de "rouler la vôtre" où vous effectuez réellement les "requêtes multiples" à la base de données vous-même au lieu d'utiliser l'assistant que .populate() est.

En utilisant la sortie de peuplement, vous pouvez ensuite simplement manipuler les données dans le résultat comme n'importe quelle autre structure de données, tant que vous appliquez .lean() à la requête pour convertir ou extraire autrement les données d'objet simples des documents mangouste renvoyés.

Bien que les approches agrégées semblent beaucoup plus complexes, il y en a "beaucoup" plus d'avantages à faire ce travail sur le serveur. Des ensembles de résultats plus volumineux peuvent être triés, des calculs peuvent être effectués pour un filtrage plus poussé et, bien sûr, vous obtenez une "réponse unique" à une "requête unique" apportées au serveur, le tout sans surcharge supplémentaire.

Il est tout à fait discutable que les pipelines eux-mêmes pourraient simplement être construits sur la base d'attributs déjà stockés sur le schéma. Donc, écrire votre propre méthode pour effectuer cette "construction" basée sur le schéma joint ne devrait pas être trop difficile.

A plus long terme bien sur $lookup est la meilleure solution, mais vous devrez probablement travailler un peu plus sur le codage initial, si bien sûr vous ne copiez pas simplement ce qui est listé ici;)