Vous devez "projeter" la correspondance ici car la requête MongoDB ne fait que rechercher un "document" contenant "au moins un élément" qui est "supérieur à" la condition que vous avez demandée.
Ainsi, filtrer un "tableau" n'est pas la même chose que la condition de "requête" que vous avez.
Une simple "projection" renverra simplement le "premier" élément correspondant à cette condition. Ce n'est donc probablement pas ce que vous voulez, mais à titre d'exemple :
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
}
)
Ce "en quelque sorte" fait ce que vous voulez, mais le problème va vraiment être qu'il ne retournera jamais qu'au plus un élément dans le "articles"
tableau.
Pour le faire correctement, vous avez besoin de .aggregate()
pour filtrer le contenu du tableau. Idéalement, cela se fait avec MongoDB 3.2 et $filter
. Mais il existe aussi un moyen spécial de .populate()
ici :
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
}
)
}
)
Donc, ce qui se passe ici, c'est que le "filtrage" réel du tableau se produit dans le .aggregate()
déclaration, mais bien sûr le résultat n'est plus un "document mangouste" car un aspect de .aggregate()
est qu'il peut "modifier" la structure du document, et pour cette raison, mangouste "présume" que c'est le cas et renvoie simplement un "objet brut".
Ce n'est pas vraiment un problème, puisque lorsque vous voyez le $project
étape, nous demandons en fait tous les mêmes champs présents dans le document selon le schéma défini. Ainsi, même s'il ne s'agit que d'un "objet simple", il n'y a aucun problème à le "reconvertir" dans un document mangouste.
C'est là que le .map()
entre en jeu, car il renvoie un tableau de "documents" convertis, ce qui est alors important pour l'étape suivante.
Maintenant, vous appelez Model.populate()
qui peut ensuite exécuter la "population" supplémentaire sur le "tableau de documents mangouste".
Le résultat est enfin ce que vous voulez.
MongoDB versions antérieures à 3.2.x
Les seules choses qui changent vraiment ici sont le pipeline d'agrégation. C'est donc tout ce qui doit être inclus par souci de brièveté.
MongoDB 2.6 - Peut filtrer les tableaux avec une combinaison de $map
et $setDifference
. Le résultat est un "ensemble" mais ce n'est pas un problème lorsque la mangouste crée un _id
champ sur tous les tableaux de sous-documents par défaut :
[
{ "$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
}}
],
Les révisions plus anciennes que celles-ci doivent utiliser $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" }
}}
],
L'alternative $lookup
Une autre alternative consiste à tout faire sur le "serveur" à la place. Ceci est une option avec $lookup
de MongoDB 3.2 et supérieur :
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) {
}
)
Et bien que ce ne soient que des documents simples, ce sont les mêmes résultats que ce que vous auriez obtenu du .populate()
approcher. Et bien sûr, vous pouvez toujours aller "caster" vers des documents de mangouste dans tous les cas si vous le devez vraiment.
Le chemin "le plus court"
Cela revient vraiment à la déclaration d'origine où vous "acceptez" simplement que la "requête" ne soit pas destinée à "filtrer" le contenu du tableau. Le .populate()
peut heureusement le faire parce que ce n'est qu'une autre "requête" et qu'elle bourre des "documents" par commodité.
Donc, si vous n'économisez vraiment pas de "bucketloads" de bande passante en supprimant des membres de tableau supplémentaires dans le tableau de document d'origine, alors juste .filter()
les sortir dans le code de post-traitement :
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
}
)