$ lookup em ObjectId's em uma matriz

103

Qual é a sintaxe para fazer uma $ lookup em um campo que é uma matriz de ObjectIds em vez de apenas um único ObjectId?

Exemplo de documento de pedido:

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

Consulta que não funciona:

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

Resultado desejado

{
  _id: ObjectId("..."),
  products: [
    ObjectId("..<Car ObjectId>.."),
    ObjectId("..<Bike ObjectId>..")
  ],
  productObjects: [
    {<Car Object>},
    {<Bike Object>}
  ],
}
Jason Lin
fonte
Meu exemplo com documento de pedido não é claro o suficiente? você gostaria de documentos de exemplo para os produtos?
Jason Lin
SERVER-22881 rastreará o funcionamento do array conforme o esperado (não como um valor literal).
Asya Kamsky

Respostas:

139

Atualização de 2017

$ lookup agora pode usar diretamente um array como o campo local . $unwindnão é mais necessário.

Resposta antiga

O $lookupestágio do pipeline de agregação não funcionará diretamente com uma matriz. O objetivo principal do projeto é fazer uma "junção à esquerda" como um tipo de junção "um para muitos" (ou realmente uma "pesquisa") nos possíveis dados relacionados. Mas o valor deve ser singular e não uma matriz.

Portanto, você deve "desnormalizar" o conteúdo antes de executar a $lookupoperação para que isso funcione. E isso significa usar $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" }
    }}
])

Depois de $lookupcorresponder a cada membro do array, o resultado é um array em si, para que você $unwindvolte $groupa $pushusar novos arrays para o resultado final.

Observe que qualquer correspondência de "junção à esquerda" que não seja encontrada criará uma matriz vazia para "productObjects" no produto fornecido e, portanto, negará o documento para o elemento "produto" quando o segundo $unwindfor chamado.

Embora uma aplicação direta a um array fosse bom, é assim que isso funciona atualmente, combinando um valor singular com muitos possíveis.

Como $lookupé basicamente muito novo, atualmente funciona como seria familiar para aqueles que estão familiarizados com o mangusto como uma "versão de homem pobre" do .populate()método oferecido lá. A diferença é que $lookupoferece processamento "do lado do servidor" da "junção" em oposição ao do cliente e que parte da "maturidade" no $lookupestá faltando no que .populate()oferece (como interpolar a pesquisa diretamente em um array).

Na verdade, esse é um problema atribuído para melhoria SERVER-22881 , então, com um pouco de sorte, chegaria à próxima versão ou logo depois.

Como princípio de design, sua estrutura atual não é boa nem ruim, mas apenas sujeita a sobrecargas ao criar qualquer "junção". Como tal, o princípio básico permanente do MongoDB no início se aplica, onde se você "pode" viver com os dados "pré-reunidos" em uma coleção, então é melhor fazer isso.

Outra coisa que pode ser dita $lookupcomo princípio geral é que a intenção da "junção" aqui é trabalhar ao contrário do mostrado aqui. Portanto, em vez de manter os "ids relacionados" dos outros documentos dentro do documento "pai", o princípio geral que funciona melhor é quando os "documentos relacionados" contêm uma referência ao "pai".

Portanto, $lookuppode-se dizer que "funciona melhor" com um "design de relação" que é o oposto de como algo como o mangusto .populate()realiza suas junções do lado do cliente. Ao idendificar o "um" dentro de cada "muitos", você apenas puxa os itens relacionados sem precisar $unwindprimeiro do array.

Blakes Seven
fonte
Obrigado, funciona! Este é um indicador de que meus dados não estão estruturados / normalizados corretamente?
Jason Lin
1
@JasonLin Não é tão direto quanto "bom / ruim", então há um pouco mais de explicação adicionada à resposta. Depende do que combina com você.
Blakes Seven
2
a implementação atual é um tanto não intencional. faz sentido procurar todos os valores em uma matriz de campo local, não faz sentido usar a matriz literalmente para que o SERVER-22881 rastreie a correção disso.
Asya Kamsky
@AsyaKamsky Isso faz sentido. Em geral, tenho tratado as consultas sobre $lookupvalidação de documentos como sendo recursos em sua infância e com probabilidade de melhorar. Portanto, a expansão direta em uma matriz seria bem-vinda, assim como uma "consulta" para filtrar os resultados. Ambos estariam muito mais alinhados com o .populate()processo do mangusto a que muitos estão acostumados. Adicionando o link do problema diretamente no conteúdo da resposta.
Blakes Seven
2
Observe que, de acordo com a resposta abaixo, isso agora foi implementado e $lookupagora funciona diretamente em um array.
Adam Reis
15

Você também pode usar o pipelineestágio para realizar verificações em uma matriz de subdocumentos

Aqui está o exemplo usando python(desculpe, sou gente cobra).

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

O problema aqui é combinar todos os objetos no ObjectId array(estrangeiro _idque está no localcampo / prop products).

Você também pode limpar ou projetar os registros estrangeiros com stages adicionais , conforme indicado pelo comentário acima.

user12164
fonte
4

use $ unfind você obterá o primeiro objeto em vez de uma matriz de objetos

inquerir:

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

resultado:

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

Agregar com $lookupe subsequente $groupé muito complicado, então se (e esse é um meio se) você estiver usando o node & Mongoose ou uma biblioteca de suporte com algumas dicas no esquema, você pode usar um .populate()para buscar esses documentos:

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")
    ...
Arco
fonte
0

Eu tenho que discordar, podemos fazer $ lookup funcionar com a matriz de IDs se começarmos com $ match stage.

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

Torna-se mais complicado se quisermos passar o resultado da pesquisa para um pipeline. Mas, novamente, há uma maneira de fazer isso (já sugerido por @ 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
fonte