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

Comment joindre à deux collections supplémentaires avec conditions

Ce qui vous manque ici, c'est que $lookup produit un "tableau" dans le champ de sortie spécifié par as dans ses arguments. C'est le concept général des "relations" MongoDB, en ce sens qu'une "relation" entre documents est représentée comme une "sous-propriété" qui se trouve "dans" le document lui-même, étant soit singulière, soit un "tableau" pour plusieurs.

Étant donné que MongoDB est "sans schéma", la présomption générale de $lookup est que vous voulez dire "beaucoup" et le résultat est donc "toujours" un tableau. Donc, à la recherche du "même résultat qu'en SQL", vous devez alors $unwind ce tableau après le $lookup . Que ce soit « un » ou « plusieurs » n'a aucune importance, car il s'agit toujours « toujours » d'un tableau :

db.getCollection.('tb1').aggregate([
  // Filter conditions from the source collection
  { "$match": { "status": { "$ne": "closed" } }},

  // Do the first join
  { "$lookup": {
    "from": "tb2",
    "localField": "id",
    "foreignField": "profileId",
    "as": "tb2"
  }},

  // $unwind the array to denormalize
  { "$unwind": "$tb2" },

  // Then match on the condtion for tb2
  { "$match": { "tb2.profile_type": "agent" } },

  // join the second additional collection
  { "$lookup": {
    "from": "tb3",
    "localField": "tb2.id",
    "foreignField": "id",
    "as": "tb3"
  }},

  // $unwind again to de-normalize
  { "$unwind": "$tb3" },

  // Now filter the condition on tb3
  { "$match": { "tb3.status": 0 } },

  // Project only wanted fields. In this case, exclude "tb2"
  { "$project": { "tb2": 0 } }
])

Ici, vous devez noter les autres éléments qui vous manquent dans la traduction :

La séquence est "importante"

Les pipelines d'agrégation sont plus « expressifs » que SQL. Ils sont en fait mieux considérés comme "une séquence d'étapes" appliquée à la source de données afin de rassembler et de transformer les données. Le meilleur analogue à cela est les instructions de ligne de commande "canalisées", telles que :

ps -ef  | grep mongod | grep -v grep | awk '{ print $1 }'

Où le "tuyau" | peut être considérée comme une "étape de pipeline" dans un "pipeline" d'agrégation MongoDB

En tant que tel, nous voulons $match afin de filtrer les éléments de la collection "source" comme première opération. Et c'est généralement une bonne pratique car cela supprime tous les documents qui ne remplissaient pas les conditions requises des autres conditions. Tout comme ce qui se passe dans notre exemple "canal de ligne de commande", où nous prenons "input" puis "pipe" vers un grep pour "supprimer" ou "filtrer".

Les chemins comptent

Où la toute prochaine chose que vous faites ici est "rejoindre" via $lookup . Le résultat est un "tableau" des éléments du "from" argument de collection correspondant aux champs fournis à afficher dans le "as" "chemin de champ" en tant que "tableau".

La dénomination choisie ici est importante, car désormais le "document" de la collection source considère que tous les éléments de la "jointure" existent désormais sur ce chemin donné. Pour faciliter cela, j'utilise le même nom de "collection" que la "jointure" pour le nouveau "chemin".

Donc, à partir du premier "join", la sortie est "tb2" et qui contiendra tous les résultats de cette collection. Il y a aussi une chose importante à noter avec la séquence suivante de $unwind puis $match , quant à la manière dont MongoDB traite réellement la requête.

Certaines séquences comptent "vraiment"

Puisqu'il "ressemble", il y a "trois" étapes de pipeline, à savoir $lookup puis $unwind puis $match . Mais en "fait" MongoDB fait vraiment autre chose, ce qui est démontré dans la sortie de { "explain": true } ajouté au .aggregate() commande :

    {
        "$lookup" : {
            "from" : "tb2",
            "as" : "tb2",
            "localField" : "id",
            "foreignField" : "profileId",
            "unwinding" : {
                "preserveNullAndEmptyArrays" : false
            },
            "matching" : {
                "profile_type" : {
                    "$eq" : "agent"
                }
            }
        }
    }, 
    {
        "$lookup" : {
            "from" : "tb3",
            "as" : "tb3",
            "localField" : "tb2.id",
            "foreignField" : "id",
            "unwinding" : {
                "preserveNullAndEmptyArrays" : false
            },
            "matching" : {
                "status" : {
                    "$eq" : 0.0
                }
            }
        }
    }, 

Donc, à part le premier point de "séquence" s'appliquant où vous devez mettre le $match où elles sont nécessaires et font le "plus grand bien", cela devient en fait "vraiment important" avec le concept de "jointures". La chose à noter ici est que nos séquences de $lookup puis $unwind puis $match , sont en fait traités par MongoDB en tant que $lookup étapes, avec les autres opérations "regroupées" dans une étape de pipeline pour chacune.

Il s'agit d'une distinction importante par rapport aux autres méthodes de "filtrage" des résultats obtenus par $lookup . Puisque dans ce cas, les conditions de "requête" réelles sur la "jointure" de $match sont effectuées sur la collection à joindre "avant" que les résultats ne soient renvoyés au parent.

Ceci en combinaison avec $unwind ( qui se traduit par unwinding ) comme indiqué ci-dessus est la façon dont MongoDB traite réellement la possibilité que la "jointure" puisse entraîner la production d'un tableau de contenu dans le document source, ce qui l'amène à dépasser la limite BSON de 16 Mo. Cela ne se produirait que dans les cas où le résultat joint est très volumineux, mais le même avantage est là où le "filtre" est réellement appliqué, étant sur la collection cible "avant" que les résultats ne soient renvoyés.

C'est ce type de traitement qui correspond le mieux au même comportement qu'un SQL JOIN. C'est donc aussi le moyen le plus efficace d'obtenir des résultats à partir d'une $lookup où il y a d'autres conditions à appliquer au JOIN en dehors simplement des valeurs de clés "locales" ou "étrangères".

Notez également que l'autre changement de comportement provient de ce qui est essentiellement un LEFT JOIN effectué par $lookup où le document "source" serait toujours conservé indépendamment de la présence d'un document correspondant dans la collection "cible". Au lieu de cela, le $unwind ajoute à cela en "éliminant" tous les résultats de la "source" qui n'avaient rien correspondant à la "cible" par les conditions supplémentaires dans $match .

En fait, ils sont même rejetés au préalable en raison de l'implicite preserveNullAndEmptyArrays: false qui est inclus et supprimerait tout ce dont les clés "locale" et "étrangère" ne correspondaient même pas entre les deux collections. C'est une bonne chose pour ce type particulier de requête car la "jointure" est destinée à être "égale" sur ces valeurs.

Conclure

Comme indiqué précédemment, MongoDB traite généralement les "relations" de manière très différente de la façon dont vous utiliseriez une "base de données relationnelle" ou un SGBDR. Le concept général de "relations" consiste en fait à "incorporer" les données, soit comme une propriété unique, soit comme un tableau.

Vous pouvez réellement désirer une telle sortie, ce qui fait également partie de la raison pour laquelle sans le $unwind séquencez ici la sortie de $lookup est en fait un "tableau". Cependant, en utilisant $unwind dans ce contexte est en fait la chose la plus efficace à faire, tout en garantissant que les données "jointes" n'entraînent pas réellement le dépassement de la limite BSON susmentionnée à la suite de cette "jointure".

Si vous voulez réellement des tableaux de sortie, alors la meilleure chose à faire ici serait d'utiliser le $group étape de pipeline, et éventuellement en plusieurs étapes afin de "normaliser" et "annuler les résultats" de $unwind

  { "$group": {
    "_id": "$_id",
    "tb1_field": { "$first": "$tb1_field" },
    "tb1_another": { "$first": "$tb1_another" },
    "tb3": { "$push": "$tb3" }    
  }}

Où vous auriez en fait pour ce cas listé tous les champs dont vous avez besoin de "tb1" par leurs noms de propriété en utilisant $first pour ne conserver que la "première" occurrence (essentiellement répétée par les résultats de "tb2" et "tb3" déroulé ) puis $push le "détail" de "tb3" dans un "tableau" pour représenter la relation avec "tb1" .

Mais la forme générale du pipeline d'agrégation telle qu'elle est donnée est la représentation exacte de la façon dont les résultats seraient obtenus à partir du SQL d'origine, qui est une sortie "dénormalisée" à la suite de la "jointure". Si vous souhaitez "normaliser" à nouveau les résultats après cela, c'est à vous de décider.