$ lookup su ObjectId in un array


103

Qual è la sintassi per eseguire una $ lookup su un campo che è un array di ObjectId piuttosto che un singolo ObjectId?

Documento d'ordine di esempio:

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

Query non funzionante:

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

Risultato desiderato

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

Il mio esempio con il documento d'ordine non è abbastanza chiaro? vorresti documenti di esempio per i prodotti?
Jason Lin

SERVER-22881 terrà traccia del funzionamento dell'array come previsto (non come valore letterale).
Asya Kamsky

Risposte:


139

Aggiornamento 2017

$ lookup può ora utilizzare direttamente un array come campo locale . $unwindnon è più necessario.

Vecchia risposta

La $lookupfase della pipeline di aggregazione non funzionerà direttamente con un array. L'intento principale del progetto è per un "join sinistro" come un tipo di join "uno a molti" (o davvero una "ricerca") sui possibili dati correlati. Ma il valore deve essere singolare e non un array.

Pertanto è necessario "de-normalizzare" il contenuto prima di eseguire l' $lookupoperazione affinché funzioni. E questo significa usare $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" }
    }}
])

Dopo la $lookupcorrispondenza di ogni membro dell'array, il risultato è un array stesso, quindi di $unwindnuovo e $groupsu $pushnuovi array per il risultato finale.

Notare che qualsiasi corrispondenza "left join" non trovata creerà un array vuoto per "productObjects" sul prodotto dato e quindi negherà il documento per l'elemento "product" quando $unwindviene chiamato il secondo .

Sebbene un'applicazione diretta a un array sarebbe carina, è proprio come funziona attualmente abbinando un valore singolare a un numero possibile.

Essendo $lookupfondamentalmente molto nuovo, attualmente funziona come sarebbe familiare a coloro che hanno familiarità con la mangusta come "versione dei poveri" del .populate()metodo offerto lì. La differenza è che $lookupoffre l'elaborazione "lato server" del "join" invece che sul client e che parte della "maturità" in $lookupè attualmente mancante di ciò che .populate()offre (come l'interpolazione della ricerca direttamente su un array).

Questo è in realtà un problema assegnato per il miglioramento SERVER-22881 , quindi con un po 'di fortuna potrebbe essere pubblicato nella versione successiva o subito dopo.

Come principio di progettazione, la tua struttura attuale non è né buona né cattiva, ma solo soggetta a spese generali durante la creazione di qualsiasi "unione". In quanto tale, si applica il principio di base di MongoDB all'inizio, in cui se "puoi" convivere con i dati "pre-uniti" in una raccolta, allora è meglio farlo.

L'altra cosa che si può dire $lookupcome principio generale, è che l'intento del "join" qui è quello di lavorare al contrario di quanto mostrato qui. Quindi, piuttosto che mantenere gli "ID correlati" degli altri documenti all'interno del documento "genitore", il principio generale che funziona meglio è dove i "documenti correlati" contengono un riferimento al "genitore".

Quindi si $lookuppuò dire che "funziona meglio" con un "progetto di relazione" che è l'opposto di come qualcosa come la mangusta .populate()esegue i suoi join lato client. Identificando invece l '"uno" all'interno di ogni "molti", è sufficiente inserire gli elementi correlati senza dover prima eseguire $unwindl'array.


Grazie funziona! È un indicatore che i miei dati non sono strutturati / normalizzati correttamente?
Jason Lin

1
@JasonLin Non così semplice come "buono / cattivo", quindi c'è un po 'più di spiegazione aggiunta alla risposta. Dipende da cosa ti si addice.
Blakes Seven

2
l'attuale implementazione è alquanto involontaria. ha senso cercare tutti i valori in un array di campi locali, non ha senso usare l'array letteralmente, quindi SERVER-22881 terrà traccia della correzione.
Asya Kamsky

@AsyaKamsky Questo ha senso. In genere ho trattato le richieste di informazioni $lookupe la convalida dei documenti come funzionalità nella loro infanzia e che probabilmente miglioreranno. Quindi un'espansione diretta su un array sarebbe benvenuta, così come una "query" per filtrare i risultati. Entrambi sarebbero molto più allineati con il .populate()processo della mangusta a cui molti sono abituati. Aggiunta del collegamento al problema direttamente nel contenuto della risposta.
Blakes Seven

2
Nota che come per la risposta sotto questa, ora è stato implementato e $lookupora funziona direttamente su un array.
Adam Reis,


15

È inoltre possibile utilizzare lo pipelinestage per eseguire controlli su un array di sottodocunzioni

Ecco l'esempio che utilizza python(scusa, sono gente serpente).

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

Il problema qui è quello di abbinare tutti gli oggetti nel ObjectId array(estraneo _idche è in localfield / prop products).

Puoi anche ripulire o proiettare i record esterni con messaggi aggiuntivi stage, come indicato dal commento sopra.


4

usa $ rilassarsi otterrai il primo oggetto invece di un array di oggetti

domanda:

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

risultato:

{
    "_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"
    }
}

0

Aggregare con $lookupe successivi $groupè piuttosto complicato, quindi se (e questo è un mezzo se) stai usando node & Mongoose o una libreria di supporto con alcuni suggerimenti nello schema, potresti usare a .populate()per recuperare quei documenti:

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")
    ...

0

Non sono d'accordo, possiamo far funzionare $ lookup con l'array IDs se lo anteponiamo con $ match stage.

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

Diventa più complicato se vogliamo passare il risultato della ricerca a una pipeline. Ma poi di nuovo c'è un modo per farlo (già suggerito da @ 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"
        }
    }
])

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.