$ Lookup für ObjectIds in einem Array

102

Wie lautet die Syntax für eine $ -Suche in einem Feld, das ein Array von ObjectIds und nicht nur eine einzelne ObjectId ist?

Beispiel für ein Bestelldokument:

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

Nicht funktionierende Abfrage:

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

Erwünschtes Ergebnis

{
  _id: ObjectId("..."),
  products: [
    ObjectId("..<Car ObjectId>.."),
    ObjectId("..<Bike ObjectId>..")
  ],
  productObjects: [
    {<Car Object>},
    {<Bike Object>}
  ],
}
Jason Lin
quelle
Ist mein Beispiel mit Bestellbeleg nicht klar genug? Möchten Sie Beispieldokumente für die Produkte?
Jason Lin
SERVER-22881 verfolgt, wie das Array wie erwartet funktioniert (nicht als Literalwert).
Asya Kamsky

Antworten:

137

Update 2017

$ lookup kann jetzt direkt ein Array als lokales Feld verwenden . $unwindwird nicht mehr benötigt.

Alte Antwort

Die $lookupPhase der Aggregationspipeline funktioniert nicht direkt mit einem Array. Die Hauptabsicht des Entwurfs besteht darin, einen "linken Join" als einen "Eins-zu-Viele" -Typ (oder wirklich einen "Lookup") für die möglichen verwandten Daten zu erstellen. Der Wert soll jedoch singulär und kein Array sein.

Daher müssen Sie den Inhalt zuerst "de-normalisieren", bevor Sie den $lookupVorgang ausführen, damit dies funktioniert. Und das bedeutet $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" }
    }}
])

Nach $lookupÜbereinstimmungen mit jedem Array-Mitglied ist das Ergebnis ein Array selbst, so dass Sie $unwinderneut und $groupzu $pushneuen Arrays für das Endergebnis.

Beachten Sie, dass alle nicht gefundenen "Left Join" -Matches ein leeres Array für die "productObjects" des angegebenen Produkts erstellen und somit das Dokument für das "product" -Element negieren, wenn das zweite $unwindaufgerufen wird.

Obwohl eine direkte Anwendung auf ein Array nett wäre, funktioniert dies derzeit nur, indem ein einzelner Wert mit einer möglichen Anzahl von Werten abgeglichen wird.

Da $lookupes im Grunde genommen sehr neu ist, funktioniert es derzeit so, wie es denjenigen bekannt ist, die mit Mungo als "Version des armen Mannes" der .populate()dort angebotenen Methode vertraut sind . Der Unterschied besteht darin, dass $lookupdie "serverseitige" Verarbeitung des "Joins" im Gegensatz zum Client angeboten wird und dass $lookupderzeit ein Teil der "Reife" fehlt.populate() Angeboten (z. B. das Interpolieren der Suche direkt auf einem Array).

Dies ist tatsächlich ein zugewiesenes Problem zur Verbesserung von SERVER-22881 . Mit etwas Glück würde dies die nächste Version oder eine bald danach treffen.

Als Entwurfsprinzip ist Ihre aktuelle Struktur weder gut noch schlecht, sondern unterliegt nur dem Overhead beim Erstellen eines "Joins". Als solches gilt das Grundprinzip von MongoDB in der Anfangszeit. Wenn Sie mit den Daten "leben" können, die in einer Sammlung "vorverknüpft" sind, ist es am besten, dies zu tun.

Das andere, was $lookupals allgemeines Prinzip gesagt werden kann , ist, dass die Absicht des "Joins" hier darin besteht, umgekehrt zu arbeiten als hier gezeigt. Anstatt die "verwandten IDs" der anderen Dokumente im "übergeordneten" Dokument zu belassen, funktioniert das allgemeine Prinzip am besten, wenn die "verwandten Dokumente" einen Verweis auf das "übergeordnete Dokument" enthalten.

Man $lookupkann also sagen, dass es mit einem "Beziehungsdesign" "am besten funktioniert", das das Gegenteil davon ist, wie etwas wie Mungo .populate()seine clientseitigen Verknüpfungen ausführt. Indem Sie stattdessen das "Eins" in jedem "Viele" identifizieren, ziehen Sie einfach die zugehörigen Elemente ein, ohne $unwindzuerst das Array zu benötigen .

Blakes Seven
quelle
Danke, dass es funktioniert! Ist dies ein Indikator dafür, dass meine Daten nicht richtig strukturiert / normalisiert sind?
Jason Lin
1
@JasonLin Nicht so direkt wie "gut / schlecht", daher wird der Antwort etwas mehr Erklärung hinzugefügt. Es kommt darauf an, was zu Ihnen passt.
Blakes Seven
2
Die derzeitige Implementierung ist etwas unbeabsichtigt. Es ist sinnvoll, alle Werte in einem Array lokaler Felder nachzuschlagen. Es ist nicht sinnvoll, das Array wörtlich zu verwenden, damit SERVER-22881 die Behebung dieses Problems nachverfolgen kann.
Asya Kamsky
@AsyaKamsky Das macht Sinn. Ich habe Anfragen $lookupund Dokumentvalidierung im Allgemeinen als Merkmale in den Kinderschuhen behandelt, die sich wahrscheinlich verbessern werden. Eine direkte Erweiterung eines Arrays wäre daher ebenso willkommen wie eine "Abfrage" zum Filtern der Ergebnisse. Beide wären viel mehr auf den Mungo- .populate()Prozess ausgerichtet, an den viele gewöhnt sind. Hinzufügen des Problemlinks direkt zum Antwortinhalt.
Blakes Seven
2
Beachten Sie, dass dies gemäß der folgenden Antwort jetzt implementiert wurde und $lookupjetzt direkt auf einem Array funktioniert.
Adam Reis
14

Sie können die pipelineStufe auch verwenden , um Überprüfungen für ein Subdokumentationsarray durchzuführen

Hier ist das Beispiel mit python(sorry, ich bin Schlangenmenschen).

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

Der Haken hier ist, alle Objekte in der ObjectId array(fremde _id, die in localFeld / Requisite ist products) abzugleichen .

Sie können die ausländischen Datensätze auch bereinigen oder mit zusätzlichen stages projizieren , wie im obigen Kommentar angegeben.

user12164
quelle
5

Mit $ unwind erhalten Sie das erste Objekt anstelle eines Array von Objekten

Abfrage:

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

Ergebnis:

{
    "_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
quelle
0

Das Aggregieren mit $lookupund im Anschluss $groupist ziemlich umständlich. Wenn Sie also Node & Mongoose oder eine unterstützende Bibliothek mit einigen Hinweisen im Schema verwenden (und das ist ein Medium, wenn), können Sie .populate()diese Dokumente mit a abrufen:

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")
    ...
Bogen
quelle
0

Ich muss nicht zustimmen, wir können dafür sorgen, dass $ lookup mit dem IDs-Array funktioniert, wenn wir ihm die $ match-Phase voranstellen.

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

Es wird komplizierter, wenn wir das Suchergebnis an eine Pipeline übergeben möchten. Andererseits gibt es eine Möglichkeit, dies zu tun (bereits von @ user12164 vorgeschlagen):

// 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
quelle