$ búsqueda en ObjectId en una matriz

103

¿Cuál es la sintaxis para realizar una búsqueda de $ en un campo que es una matriz de ObjectIds en lugar de un solo ObjectId?

Ejemplo de documento de pedido:

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

Consulta que no funciona:

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

Resultado deseado

{
  _id: ObjectId("..."),
  products: [
    ObjectId("..<Car ObjectId>.."),
    ObjectId("..<Bike ObjectId>..")
  ],
  productObjects: [
    {<Car Object>},
    {<Bike Object>}
  ],
}
Jason Lin
fuente
¿Mi ejemplo con el documento de pedido no es lo suficientemente claro? ¿Quiere documentos de ejemplo para los productos?
Jason Lin
SERVER-22881 rastreará el funcionamiento de la matriz como se esperaba (no como un valor literal).
Asya Kamsky

Respuestas:

139

Actualización de 2017

$ lookup ahora puede usar directamente una matriz como campo local . $unwindya no es necesario.

Respuesta antigua

La $lookupetapa de canalización agregación no funcionará directamente con una matriz. La intención principal del diseño es una "combinación izquierda" como un tipo de combinación "uno a muchos" (o realmente una "búsqueda") de los posibles datos relacionados. Pero el valor está destinado a ser singular y no una matriz.

Por lo tanto, primero debe "desnormalizar" el contenido antes de realizar la $lookupoperación para que esto funcione. Y eso 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" }
    }}
])

Después de que $lookupcoincida con cada miembro de la matriz, el resultado es una matriz en sí misma, por lo que $unwindnuevamente y $groupa $pushnuevas matrices para el resultado final.

Tenga en cuenta que cualquier coincidencia de "unión izquierda" que no se encuentre creará una matriz vacía para los "productObjects" en el producto dado y, por lo tanto, anulará el documento para el elemento "producto" cuando $unwindse llame al segundo .

Aunque una aplicación directa a una matriz estaría bien, así es como funciona actualmente al hacer coincidir un valor singular con varios posibles.

Como $lookupes básicamente muy nuevo, actualmente funciona como sería familiar para aquellos que están familiarizados con la mangosta como una "versión de hombre pobre" del .populate()método que se ofrece allí. La diferencia es que $lookupofrece el procesamiento del "lado del servidor" de la "unión" en contraposición al del cliente y que parte de la "madurez" $lookupfalta actualmente en las .populate()ofertas (como interpolar la búsqueda directamente en una matriz).

En realidad, este es un problema asignado para mejorar SERVER-22881 , por lo que, con algo de suerte, llegaría a la próxima versión o poco después.

Como principio de diseño, su estructura actual no es ni buena ni mala, sino que está sujeta a gastos generales al crear cualquier "unión". Como tal, se aplica el principio básico de MongoDB en el inicio, donde si "puede" vivir con los datos "pre-unidos" en una colección, entonces es mejor hacerlo.

La otra cosa que se puede decir $lookupcomo principio general es que la intención de la "unión" aquí es trabajar al revés de lo que se muestra aquí. Entonces, en lugar de mantener los "ID relacionados" de los otros documentos dentro del documento "principal", el principio general que funciona mejor es que los "documentos relacionados" contienen una referencia al "padre".

Por tanto, $lookupse puede decir que "funciona mejor" con un "diseño de relación" que es lo contrario de cómo algo como la mangosta .populate()realiza sus uniones del lado del cliente. Al identificar el "uno" dentro de cada "muchos" en su lugar, simplemente extrae los elementos relacionados sin necesidad de $unwindla matriz primero.

Siete de blakes
fuente
¡Gracias, funciona! ¿Es esto un indicador de que mis datos no están estructurados / normalizados correctamente?
Jason Lin
1
@JasonLin No es tan sencillo como "bueno / malo", por lo que se agrega un poco más de explicación a la respuesta. Depende de lo que más te convenga.
Blakes Seven
2
la implementación actual es algo involuntaria. tiene sentido buscar todos los valores en una matriz de campo local, no tiene sentido usar la matriz literalmente, por lo que SERVER-22881 rastreará la corrección de eso.
Asya Kamsky
@AsyaKamsky Eso tiene sentido. En general, he tratado las consultas $lookupy la validación de documentos como características en su infancia y que probablemente mejorarán. Por tanto, la expansión directa en una matriz sería bienvenida, al igual que una "consulta" para filtrar resultados. Ambos estarían mucho más alineados con el .populate()proceso de la mangosta al que muchos están acostumbrados. Agregar el enlace del problema directamente en el contenido de la respuesta.
Blakes Seven
2
Tenga en cuenta que según la respuesta a continuación, esto ahora se ha implementado y $lookupahora funciona directamente en una matriz.
Adam Reis
15

También puede utilizar el pipelineescenario para realizar comprobaciones en una matriz de subdocumentos.

Aquí está el ejemplo usando python(lo siento, soy gente serpiente).

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

La trampa aquí es hacer coincidir todos los objetos en el ObjectId array(extraño _idque está en el localcampo / prop products).

También puede limpiar o proyectar los registros externos con correos stageelectrónicos adicionales , como se indica en el comentario anterior.

user12164
fuente
4

usar $ relajarse obtendrá el primer objeto en lugar de una matriz de objetos

consulta:

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
fuente
0

La agregación con $lookupy subsiguiente $groupes bastante engorrosa, por lo que si (y eso es un medio si) está usando node & Mongoose o una biblioteca de soporte con algunas sugerencias en el esquema, podría usar a .populate()para buscar esos 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
fuente
0

Tengo que estar en desacuerdo, podemos hacer que $ lookup funcione con la matriz de ID si lo precedimos 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"
        }
    }
])

Se vuelve más complicado si queremos pasar el resultado de la búsqueda a una canalización. Pero, de nuevo, hay una manera de hacerlo (ya 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
fuente