Mongoose Query para filtrar una matriz y rellenar contenido relacionado

Estoy intentando consultar la propiedad que es una matriz de referencia a otro esquema y algunos datos adicionales. Para una mejor aclaración, aquí está el esquema:

var orderSchema = new Schema({ orderDate: Date, articles: [{ article: { type: Schema.Types.ObjectId, ref: 'Article' }, quantity: 'Number' }] }), Order = mongoose.model('Order', orderSchema); 

Mientras me las arreglé para consultar con éxito la referencia, es decir:

 Order.find({}).populate('articles.article', null, { price: { $lte: 500 } }).exec(function(err, data) { for (var order of data) { for (var article of order.articles) { console.log(article); } } }); 

Tengo algunos problemas al consultar el atributo de quantity , es decir, esto no funciona:

 Order.find({}).where({ 'articles.quantity': { $gte: 5 } }).populate('articles.article', null, { /*price: { $lte: 500 }*/ }).exec(function(err, data) { for (var order of data) { for (var article of order.articles) { console.log(article); } } }); 

¿Es posible incluso basar la consulta en la quantity ? Y si es así, ¿cuál sería el mejor enfoque?

¡Gracias!

ACTUALIZAR:

El problema es que el resultado es una matriz completa o nada (ver pregunta actualizada). Quiero obtener solo aquellos registros que tienen una cantidad mayor o igual a 5. Con su enfoque (y el mío) no obtengo ningún registro (si establecí $ gte: 5001) o ambos registros (si establecí $ gte: 5000)

 { "_id": ObjectId('56fe76c12f7174ac5018054f'), "orderDate": ISODate('2016-04-01T13:25:21.055Z'), "articles": [ { "article": ObjectId('56fe76c12f7174ac5018054b'), "quantity": 5000, "_id": ObjectId('56fe76c12f7174ac50180551') }, { "article": ObjectId('56fe76c12f7174ac5018054c'), "quantity": 1, "_id": ObjectId('56fe76c12f7174ac50180552') } ], "__v": 1 } 

Debe “proyectar” la coincidencia aquí, ya que toda la consulta de MongoDB es buscar un “documento” que tenga “al menos un elemento” que sea “mayor que” la condición que solicitó.

Por lo tanto, filtrar una “matriz” no es lo mismo que la condición de “consulta” que tiene.

Una simple “proyección” solo devolverá el “primer” artículo coincidente a esa condición. Probablemente no sea lo que quieres, pero como ejemplo:

 Order.find({ "articles.quantity": { "$gte": 5 } }) .select({ "articles.$": 1 }) .populate({ "path": "articles.article", "match": { "price": { "$lte": 500 } } }).exec(function(err,orders) { // populated and filtered twice } ) 

Ese “tipo de” hace lo que usted quiere, pero el problema realmente será que solo devolverá a lo sumo un elemento dentro de la matriz de "articles" .

Para hacer esto correctamente, necesita .aggregate() para filtrar el contenido de la matriz. Idealmente, esto se hace con MongoDB 3.2 y $filter . Pero también hay una manera especial de .populate() aquí:

 Order.aggregate( [ { "$match": { "artciles.quantity": { "$gte": 5 } } }, { "$project": { "orderdate": 1, "articles": { "$filter": { "input": "$articles", "as": "article", "cond": { "$gte": [ "$$article.quantity", 5 ] } } }, "__v": 1 }} ], function(err,orders) { Order.populate( orders.map(function(order) { return new Order(order) }), { "path": "articles.article", "match": { "price": { "$lte": 500 } } }, function(err,orders) { // now it's all populated and mongoose documents } ) } ) 

Entonces, lo que sucede aquí es el “filtrado” real de la matriz dentro de la statement .aggregate() , pero, por supuesto, el resultado ya no es un “documento de mongoose” porque un aspecto de .aggregate() es que puede ” Altere “la estructura del documento, y por este motivo la mongoose” presume “ese es el caso y simplemente devuelve un” objeto simple “.

Eso no es realmente un problema, ya que cuando ve la etapa $project , en realidad estamos solicitando todos los mismos campos presentes en el documento de acuerdo con el esquema definido. Por lo tanto, aunque solo sea un “objeto simple”, no hay problema en “devolverlo” a un documento de mongoose.

Aquí es donde entra en .map() , ya que devuelve una serie de “documentos” convertidos, que es importante para la siguiente etapa.

Ahora llama a Model.populate() que puede ejecutar la “población” adicional en el “conjunto de documentos de mongoose”.

El resultado entonces es finalmente lo que quieres.


MongoDB versiones anteriores a 3.2.x

Las únicas cosas que realmente cambian aquí son el canal de agregación, así que eso es todo lo que necesita ser incluido para la brevedad.

MongoDB 2.6 – Puede filtrar arreglos con una combinación de $map y $setDifference . El resultado es un “conjunto”, pero eso no es un problema cuando mongoose crea un campo _id en todas las matrices de sub-documentos de forma predeterminada:

  [ { "$match": { "artciles.quantity": { "$gte": 5 } } }, { "$project": { "orderdate": 1, "articles": { "$setDiffernce": [ { "$map": { "input": "$articles", "as": "article", "in": { "$cond": [ { "$gte": [ "$$article.price", 5 ] }, "$$article", false ] } }}, [false] ] }, "__v": 1 }} ], 

Revisiones más antiguas que deben usar $unwind :

  [ { "$match": { "artciles.quantity": { "$gte": 5 } }}, { "$unwind": "$articles" }, { "$match": { "artciles.quantity": { "$gte": 5 } }}, { "$group": { "_id": "$_id", "orderdate": { "$first": "$orderdate" }, "articles": { "$push": "$articles" }, "__v": { "$first": "$__v" } }} ], 

La alternativa de búsqueda $

Otra alternativa es simplemente hacer todo en el “servidor” en su lugar. Esta es una opción con $lookup de MongoDB 3.2 y superior:

 Order.aggregate( [ { "$match": { "artciles.quantity": { "$gte": 5 } }}, { "$project": { "orderdate": 1, "articles": { "$filter": { "input": "$articles", "as": "article", "cond": { "$gte": [ "$$article.quantity", 5 ] } } }, "__v": 1 }}, { "$unwind": "$articles" }, { "$lookup": { "from": "articles", "localField": "articles.article", "foreignField": "_id", "as": "articles.article" }}, { "$unwind": "$articles.article" }, { "$group": { "_id": "$_id", "orderdate": { "$first": "$orderdate" }, "articles": { "$push": "$articles" }, "__v": { "$first": "$__v" } }}, { "$project": { "orderdate": 1, "articles": { "$filter": { "input": "$articles", "as": "article", "cond": { "$lte": [ "$$article.article.price", 500 ] } } }, "__v": 1 }} ], function(err,orders) { } ) 

Y aunque esos son documentos simples, son los mismos resultados que obtendrías con el enfoque .populate() . Y, por supuesto, siempre puede ir y “emitir” documentos de mongoose en todos los casos, si es necesario.

El camino “más corto”

Esto realmente se remonta a la statement original donde básicamente “acepta” que la “consulta” no pretende “filtrar” el contenido de la matriz. El .populate() puede hacerlo felizmente porque es solo otra “consulta” y se está rellenando en “documentos” por conveniencia.

Por lo tanto, si realmente no está guardando “cargas compartidas” de ancho de banda al eliminar miembros de la matriz adicionales en la matriz de documentos original, simplemente .filter() en el código de procesamiento posterior:

 Order.find({ "articles.quantity": { "$gte": 5 } }) .populate({ "path": "articles.article", "match": { "price": { "$lte": 500 } } }).exec(function(err,orders) { orders = orders.filter(function(order) { order.articles = order.articles.filter(function(article) { return ( ( article.quantity >= 5 ) && ( article.article != null ) ) }); return order.aricles.length > 0; }) // orders has non matching entries removed } )