$ lookup na ObjectId w tablicy

104

Jaka jest składnia wyszukiwania $ na polu będącym tablicą ObjectId, a nie tylko pojedynczym ObjectId?

Przykładowy dokument zamówienia:

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

Niedziałające zapytanie:

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

Pożądany rezultat

{
  _id: ObjectId("..."),
  products: [
    ObjectId("..<Car ObjectId>.."),
    ObjectId("..<Bike ObjectId>..")
  ],
  productObjects: [
    {<Car Object>},
    {<Bike Object>}
  ],
}
Jason Lin
źródło
Czy mój przykład z dokumentem zamówienia nie jest wystarczająco jasny? chciałbyś mieć przykładowe dokumenty do produktów?
Jason Lin
SERVER-22881 będzie śledzić, czy tablica działa zgodnie z oczekiwaniami (nie jako wartość literału).
Asya Kamsky

Odpowiedzi:

142

Aktualizacja 2017

$ lookup może teraz bezpośrednio używać tablicy jako pola lokalnego . $unwindnie jest już potrzebne.

Stara odpowiedź

Etap $lookuppotoku agregacji nie będzie działał bezpośrednio z tablicą. Głównym założeniem projektu jest połączenie „lewe” jako „połączenie jeden do wielu” (lub w rzeczywistości „wyszukiwanie”) na możliwych powiązanych danych. Ale wartość ma być pojedyncza, a nie tablicą.

Dlatego przed wykonaniem $lookupoperacji należy najpierw „zdenormalizować” zawartość , aby to zadziałało. A to oznacza użycie $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" }
    }}
])

Po $lookupdopasowaniu każdego elementu tablicy wynik jest samą tablicą, więc $unwindponownie i $groupdo $pushnowych tablic dla wyniku końcowego.

Zwróć uwagę, że wszelkie dopasowania „left join”, które nie zostaną znalezione, utworzą pustą tablicę dla „productObjects” w danym produkcie, a tym samym zanegują dokument dla elementu „product” po $unwindwywołaniu drugiego .

Chociaż bezpośrednie zastosowanie do tablicy byłoby fajne, tak właśnie działa to obecnie, dopasowując pojedynczą wartość do możliwych wielu.

Ponieważ $lookupjest w zasadzie bardzo nowy, obecnie działa tak, jak byłoby to znane tym, którzy są zaznajomieni z mangustą jako „wersją dla biednych ludzi” .populate()oferowanej tam metody. Różnica polega na tym, że $lookupoferuje „po stronie serwera” przetwarzanie „złączenia” w przeciwieństwie do klienta, a część „dojrzałości” $lookupjest obecnie niedostępna w .populate()ofertach (np. Interpolacja wyszukiwania bezpośrednio na tablicy).

W rzeczywistości jest to przypisany problem do ulepszenia SERVER-22881 , więc przy odrobinie szczęścia pojawi się on w następnej wersji lub wkrótce później.

Zgodnie z zasadą projektową, Twoja obecna struktura nie jest ani dobra, ani zła, ale po prostu podlega kosztom ogólnym podczas tworzenia dowolnego „łączenia”. W związku z tym obowiązuje podstawowa zasada MongoDB na początku, gdzie „można” żyć z danymi „wstępnie połączonymi” w jednej kolekcji, najlepiej jest to zrobić.

Inną rzeczą, o której można powiedzieć $lookupjako ogólną zasadę, jest to, że celem „złączenia” jest tutaj działanie na odwrót, niż pokazano tutaj. Dlatego zamiast przechowywać „powiązane identyfikatory” innych dokumentów w dokumencie „nadrzędnym”, ogólną zasadą, która działa najlepiej, jest sytuacja, w której „powiązane dokumenty” zawierają odniesienie do „nadrzędnego”.

$lookupMożna więc powiedzieć, że „działa najlepiej” z „projektem relacji”, który jest odwrotnością tego, jak coś takiego jak mangusta .populate()wykonuje łączenia po stronie klienta. Zamiast tego identyfikując „jeden” w każdym „wielu”, po prostu pobierasz powiązane elementy bez konieczności wcześniejszego korzystania $unwindz tablicy.

Blakes Seven
źródło
Dziękuję, działa! Czy to oznacza, że ​​moje dane nie są odpowiednio ustrukturyzowane / znormalizowane?
Jason Lin
1
@JasonLin Nie tak proste jak „dobry / zły”, więc do odpowiedzi dodano trochę więcej wyjaśnień. To zależy od tego, co Ci odpowiada.
Blakes Seven
2
obecna implementacja jest nieco niezamierzona. ma sens wyszukanie wszystkich wartości w tablicy pola lokalnego, nie ma sensu używać tablicy dosłownie, więc SERVER-22881 będzie śledzić poprawianie tego.
Asya Kamsky
@AsyaKamsky To ma sens. Generalnie traktuję zapytania ponownie $lookupi walidację dokumentów jako funkcje w powijakach i prawdopodobnie ulegną poprawie. Dlatego mile widziane byłoby bezpośrednie rozwijanie tablicy, podobnie jak „zapytanie” do filtrowania wyników. Oba byłyby znacznie bardziej dostosowane do .populate()procesu mangusty , do którego wielu jest przyzwyczajonych. Dodanie linku do problemu bezpośrednio w treści odpowiedzi.
Blakes Seven
2
Zauważ, że zgodnie z odpowiedzią poniżej, zostało to zaimplementowane i $lookupteraz działa bezpośrednio na tablicy.
Adam Reis
15

Możesz również użyć pipelinestage do sprawdzania tablicy podrzędnej

Oto przykład użycia python(przepraszam, że jestem wężem).

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

Połów tutaj polega na dopasowaniu wszystkich obiektów w ObjectId array(obcym _id, czyli w localpolu / rekwizycie products).

Możesz również wyczyścić lub wyświetlić zagraniczne rekordy z dodatkowymi stage, jak wskazano w powyższym komentarzu.

user12164
źródło
4

użyj $ relax otrzymasz pierwszy obiekt zamiast tablicy obiektów

pytanie:

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

wynik:

{
    "_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
źródło
0

Agregowanie z $lookupi kolejne $groupjest dość uciążliwe, więc jeśli (i jest to medium, jeśli) używasz węzła i Mongoose lub biblioteki pomocniczej z pewnymi wskazówkami w schemacie, możesz użyć a, .populate()aby pobrać te dokumenty:

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")
    ...
Łuk
źródło
0

Muszę się nie zgodzić, możemy sprawić, aby wyszukiwanie $ lookup działało z tablicą IDs, jeśli poprzedzimy go etapem $ match.

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

Sytuacja staje się bardziej skomplikowana, jeśli chcemy przekazać wynik wyszukiwania do potoku. Ale z drugiej strony jest na to sposób (już zasugerowany przez @ 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
źródło