$ lookup sur ObjectId dans un tableau

103

Quelle est la syntaxe pour effectuer une recherche $ sur un champ qui est un tableau d'ObjectId plutôt qu'un seul ObjectId?

Exemple de bon de commande:

{
  _id: ObjectId("..."),
  products: [
    ObjectId("..<Car ObjectId>.."),
    ObjectId("..<Bike ObjectId>..")
  ]
}

Requête ne fonctionnant pas:

db.orders.aggregate([
    {
       $lookup:
         {
           from: "products",
           localField: "products",
           foreignField: "_id",
           as: "productObjects"
         }
    }
])

Résultat désiré

{
  _id: ObjectId("..."),
  products: [
    ObjectId("..<Car ObjectId>.."),
    ObjectId("..<Bike ObjectId>..")
  ],
  productObjects: [
    {<Car Object>},
    {<Bike Object>}
  ],
}
Jason Lin
la source
Mon exemple avec le document de commande n'est-il pas assez clair? voulez-vous des exemples de documents pour les produits?
Jason Lin
SERVER-22881 suivra le fonctionnement du tableau comme prévu (pas comme une valeur littérale).
Asya Kamsky

Réponses:

141

Mise à jour 2017

$ lookup peut maintenant utiliser directement un tableau comme champ local . $unwindn'est plus nécessaire.

Ancienne réponse

L' $lookupétape du pipeline d'agrégation ne fonctionnera pas directement avec un tableau. L'intention principale de la conception est une «jointure gauche» comme un type de jointure «un à plusieurs» (ou vraiment une «recherche») sur les données associées possibles. Mais la valeur est destinée à être singulière et non à un tableau.

Par conséquent, vous devez d'abord «dé-normaliser» le contenu avant d'effectuer l' $lookupopération pour que cela fonctionne. Et cela signifie utiliser $unwind:

db.orders.aggregate([
    // Unwind the source
    { "$unwind": "$products" },
    // Do the lookup matching
    { "$lookup": {
       "from": "products",
       "localField": "products",
       "foreignField": "_id",
       "as": "productObjects"
    }},
    // Unwind the result arrays ( likely one or none )
    { "$unwind": "$productObjects" },
    // Group back to arrays
    { "$group": {
        "_id": "$_id",
        "products": { "$push": "$products" },
        "productObjects": { "$push": "$productObjects" }
    }}
])

Après avoir $lookupcorrespondu à chaque membre du tableau, le résultat est un tableau lui-même, donc vous à $unwindnouveau et $groupà de $pushnouveaux tableaux pour le résultat final.

Notez que toute correspondance "jointure gauche" non trouvée créera un tableau vide pour les "productObjects" sur le produit donné et annulera ainsi le document pour l'élément "product" lorsque le second $unwindsera appelé.

Bien qu'une application directe à un tableau serait bien, c'est juste la façon dont cela fonctionne actuellement en faisant correspondre une valeur singulière à un nombre possible.

Comme il $lookupest fondamentalement très nouveau, il fonctionne actuellement comme le ferait familier à ceux qui sont familiers avec la mangouste en tant que "version pour les pauvres" de la .populate()méthode qui y est proposée. La différence étant qu'il $lookupoffre un traitement "côté serveur" de la "jointure" par opposition au client et qu'une partie de la "maturité" $lookupfait actuellement défaut dans les .populate()offres (comme l'interpolation de la recherche directement sur un tableau).

C'est en fait un problème assigné pour l'amélioration SERVER-22881 , donc avec un peu de chance cela arriverait dans la prochaine version ou peu de temps après.

En tant que principe de conception, votre structure actuelle n'est ni bonne ni mauvaise, mais juste sujette à des frais généraux lors de la création d'une «jointure». En tant que tel, le principe de base de MongoDB dans sa création s'applique, où si vous "pouvez" vivre avec les données "pré-jointes" dans une seule collection, alors il est préférable de le faire.

Une autre chose qui peut être considérée $lookupcomme un principe général, c'est que l'intention de la "jointure" ici est de travailler dans le sens inverse de celui montré ici. Ainsi, plutôt que de conserver les "identifiants associés" des autres documents dans le document "parent", le principe général qui fonctionne le mieux est que les "documents associés" contiennent une référence au "parent".

On $lookuppeut donc dire que "fonctionne mieux" avec une "conception de relations" qui est l'inverse de la façon dont quelque chose comme la mangouste .populate()effectue ses jointures côté client. En identifiant le "un" dans chaque "plusieurs" à la place, vous tirez simplement les éléments associés sans avoir besoin d'abord $unwinddu tableau.

Blakes Seven
la source
Merci ça marche! Est-ce un indicateur que mes données ne sont pas structurées / normalisées correctement?
Jason Lin
1
@JasonLin Pas aussi direct que "bon / mauvais", donc il y a un peu plus d'explications ajoutées à la réponse. Cela dépend de ce qui vous convient.
Blakes Seven
2
la mise en œuvre actuelle est quelque peu involontaire. il est logique de rechercher toutes les valeurs dans un tableau de champ local, cela n'a pas de sens d'utiliser le tableau littéralement donc SERVER-22881 suivra la résolution de cela.
Asya Kamsky
@AsyaKamsky Cela a du sens. J'ai généralement traité les demandes de renseignements $lookupet la validation de documents comme des fonctionnalités à leurs débuts et susceptibles de s'améliorer. Une expansion directe sur un tableau serait donc la bienvenue, tout comme une «requête» pour filtrer les résultats. Ces deux éléments seraient beaucoup plus alignés sur le .populate()processus de la mangouste auquel beaucoup sont habitués. Ajouter le lien du problème directement dans le contenu de la réponse.
Blakes Seven
2
Notez que selon la réponse ci-dessous celle-ci, cela a maintenant été implémenté et $lookupfonctionne maintenant directement sur un tableau.
Adam Reis
15

Vous pouvez également utiliser la pipelinescène pour effectuer des vérifications sur un tableau de sous-documents

Voici l'exemple utilisant python(désolé, je suis des serpents).

db.products.aggregate([
  { '$lookup': {
      'from': 'products',
      'let': { 'pid': '$products' },
      'pipeline': [
        { '$match': { '$expr': { '$in': ['$_id', '$$pid'] } } }
        // Add additional stages here 
      ],
      'as':'productObjects'
  }
])

Le hic ici est de faire correspondre tous les objets dans le ObjectId array(étranger _idqui est dans le localchamp / prop products).

Vous pouvez également nettoyer ou projeter les enregistrements étrangers avec des stages supplémentaires , comme indiqué dans le commentaire ci-dessus.

user12164
la source
4

utilisez $ unwind vous obtiendrez le premier objet au lieu d'un tableau d'objets

requete:

db.getCollection('vehicles').aggregate([
  {
    $match: {
      status: "AVAILABLE",
      vehicleTypeId: {
        $in: Array.from(newSet(d.vehicleTypeIds))
      }
    }
  },
  {
    $lookup: {
      from: "servicelocations",
      localField: "locationId",
      foreignField: "serviceLocationId",
      as: "locations"
    }
  },
  {
    $unwind: "$locations"
  }
]);

résultat:

{
    "_id" : ObjectId("59c3983a647101ec58ddcf90"),
    "vehicleId" : "45680",
    "regionId" : 1.0,
    "vehicleTypeId" : "10TONBOX",
    "locationId" : "100",
    "description" : "Isuzu/2003-10 Ton/Box",
    "deviceId" : "",
    "earliestStart" : 36000.0,
    "latestArrival" : 54000.0,
    "status" : "AVAILABLE",
    "accountId" : 1.0,
    "locations" : {
        "_id" : ObjectId("59c3afeab7799c90ebb3291f"),
        "serviceLocationId" : "100",
        "regionId" : 1.0,
        "zoneId" : "DXBZONE1",
        "description" : "Masafi Park Al Quoz",
        "locationPriority" : 1.0,
        "accountTypeId" : 0.0,
        "locationType" : "DEPOT",
        "location" : {
            "makani" : "",
            "lat" : 25.123091,
            "lng" : 55.21082
        },
        "deliveryDays" : "MTWRFSU",
        "timeWindow" : {
            "timeWindowTypeId" : "1"
        },
        "address1" : "",
        "address2" : "",
        "phone" : "",
        "city" : "",
        "county" : "",
        "state" : "",
        "country" : "",
        "zipcode" : "",
        "imageUrl" : "",
        "contact" : {
            "name" : "",
            "email" : ""
        },
        "status" : "",
        "createdBy" : "",
        "updatedBy" : "",
        "updateDate" : "",
        "accountId" : 1.0,
        "serviceTimeTypeId" : "1"
    }
}


{
    "_id" : ObjectId("59c3983a647101ec58ddcf91"),
    "vehicleId" : "81765",
    "regionId" : 1.0,
    "vehicleTypeId" : "10TONBOX",
    "locationId" : "100",
    "description" : "Hino/2004-10 Ton/Box",
    "deviceId" : "",
    "earliestStart" : 36000.0,
    "latestArrival" : 54000.0,
    "status" : "AVAILABLE",
    "accountId" : 1.0,
    "locations" : {
        "_id" : ObjectId("59c3afeab7799c90ebb3291f"),
        "serviceLocationId" : "100",
        "regionId" : 1.0,
        "zoneId" : "DXBZONE1",
        "description" : "Masafi Park Al Quoz",
        "locationPriority" : 1.0,
        "accountTypeId" : 0.0,
        "locationType" : "DEPOT",
        "location" : {
            "makani" : "",
            "lat" : 25.123091,
            "lng" : 55.21082
        },
        "deliveryDays" : "MTWRFSU",
        "timeWindow" : {
            "timeWindowTypeId" : "1"
        },
        "address1" : "",
        "address2" : "",
        "phone" : "",
        "city" : "",
        "county" : "",
        "state" : "",
        "country" : "",
        "zipcode" : "",
        "imageUrl" : "",
        "contact" : {
            "name" : "",
            "email" : ""
        },
        "status" : "",
        "createdBy" : "",
        "updatedBy" : "",
        "updateDate" : "",
        "accountId" : 1.0,
        "serviceTimeTypeId" : "1"
    }
}
KARTHIKEYAN.A
la source
0

L'agrégation avec $lookupet après $groupest assez lourde, donc si (et c'est un moyen si) vous utilisez node & Mongoose ou une bibliothèque de support avec quelques indices dans le schéma, vous pouvez utiliser a .populate()pour récupérer ces documents:

var mongoose = require("mongoose"),
    Schema = mongoose.Schema;

var productSchema = Schema({ ... });

var orderSchema = Schema({
  _id     : Number,
  products: [ { type: Schema.Types.ObjectId, ref: "Product" } ]
});

var Product = mongoose.model("Product", productSchema);
var Order   = mongoose.model("Order", orderSchema);

...

Order
    .find(...)
    .populate("products")
    ...
Arc
la source
0

Je ne suis pas d'accord, nous pouvons faire fonctionner $ lookup avec le tableau d'ID si nous le précédons avec $ match stage.

// replace IDs array with lookup results
db.products.aggregate([
    { $match: { products : { $exists: true } } },
    {
        $lookup: {
            from: "products",
            localField: "products",
            foreignField: "_id",
            as: "productObjects"
        }
    }
])

Cela devient plus compliqué si nous voulons transmettre le résultat de la recherche à un pipeline. Mais là encore, il existe un moyen de le faire (déjà suggéré par @ user12164):

// replace IDs array with lookup results passed to pipeline
db.products.aggregate([
    { $match: { products : { $exists: true } } },
    {
        $lookup: {
            from: "products",
             let: { products: "$products"},
             pipeline: [
                 { $match: { $expr: {$in: ["$_id", "$$products"] } } },
                 { $project: {_id: 0} } // suppress _id
             ],
            as: "productObjects"
        }
    }
])

Liebster Kamerad
la source