sql >> Base de Datos >  >> NoSQL >> MongoDB

Mongoose Query para filtrar una matriz y poblar contenido relacionado

Debe "proyectar" la coincidencia aquí, ya que todo lo que hace la consulta de MongoDB es buscar un "documento" que tenga "al menos un elemento" que es "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" simplemente devolverá el "primer" elemento coincidente a esa condición. Así que 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 "más o menos" hace lo que quieres, pero el problema realmente será que solo regresará a lo sumo uno elemento dentro de los "articles" matriz.

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 forma 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 que el "filtrado" real de la matriz ocurre dentro de .aggregate() pero, por supuesto, el resultado de esto ya no es un "documento de mangosta" porque un aspecto de .aggregate() es que puede "alterar" la estructura del documento, y por esta razón mongoose "presume" que ese es el caso y simplemente devuelve un "objeto simple".

Eso no es realmente un problema, ya que cuando ves el $project etapa, en realidad estamos solicitando todos los mismos campos presentes en el documento de acuerdo con el esquema definido. Entonces, aunque es solo un "objeto simple", no hay problema en "convertirlo" nuevamente en un documento de mangosta.

Aquí es donde el .map() entra, ya que devuelve una serie de "documentos" convertidos, que luego son importantes para la siguiente etapa.

Ahora llamas a Model.populate() que luego puede ejecutar la "población" adicional en la "matriz de documentos de mangosta".

El resultado entonces es finalmente lo que quieres.

MongoDB versiones anteriores a 3.2.x

Lo único que realmente cambia aquí es la canalización de agregación, por lo que eso es todo lo que debe incluirse por brevedad.

MongoDB 2.6 - Puede filtrar matrices con una combinación de $map y $setDifference . El resultado es un "conjunto", pero eso no es un problema cuando mongoose crea un _id campo en todas las matrices de subdocumentos 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
        }}
    ],

Las revisiones anteriores 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". 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 solo documentos simples, son los mismos resultados que habría obtenido de .populate() Acercarse. Y, por supuesto, siempre puede ir y "transmitir" documentos de mangosta en todos los casos nuevamente si realmente debe hacerlo.

El camino "más corto"

Esto realmente se remonta a la declaración original en la que básicamente "acepta" que la "consulta" no está destinada a "filtrar" el contenido de la matriz. El .populate() felizmente puede hacerlo porque es solo otra "consulta" y está rellenando "documentos" por conveniencia.

Entonces, si realmente no está ahorrando "cargas" de ancho de banda mediante la eliminación de miembros adicionales de la matriz en la matriz de documentos original, entonces 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            
    }
)