I want to return a specific subdocument which is nested in a second level array.
My problems are:
- The document is deeply nested
- The fields (keys) are dynamic and based on
req.bodyvalues
The collection looks something like this (simplified values for the example):
leagues:[
{
"_id": "693ae6dc320b2bb92635361e",
"cards": [
{
"_id": "600056266",
"users": [
{
"_id": "EafXiJktdKSM45OcnniU4gpcGWc2",
"vote": {
"234": {
"W": "56",
"M": "543",
"R": "322"
},
"235": {
"W": "554",
"M": "332",
"R": "454"
}
}
},
{
"_id": "EafXiJktdKSM45OcnniU4gpcGWPR4",
"vote": {
"234": {
"W": "54",
"M": "69",
"R": "987"
},
"235": {
"W": "85",
"M": "751",
"R": "65"
}
}
}
]
}
]
},
{
"_id": "693ae6dc320b2bb92633ew23",
"cards": [
{
"_id": "600056266",
"users": [
{
"_id": "EafXiJktdKSM45OcnniU4gpcGWc2",
"vote": {
"234": {
"W": "56",
"M": "543",
"R": "322"
},
"235": {
"W": "554",
"M": "332",
"R": "454"
}
}
},
{
"_id": "EafXiJktdKSM45OcnniU4gpcGWPR4",
"vote": {
"234": {
"W": "54",
"M": "69",
"R": "987"
},
"235": {
"W": "85",
"M": "751",
"R": "65"
}
}
}
]
}
]
}
]
I would like to write a query that could e.g. return the vote object from user with "_id": "EafXiJktdKSM45OcnniU4gpcGWc2" for card "_id": "600056266" inside league with "_id": "693ae6dc320b2bb92635361e".
I have spent hours and hours looking for some solution, but anything I have tried is not satisfying ($match, $elemMatch, projections).
Projection seems to work but only return the value from the element and not the complete 'vote' object, $match seems to work only for first level, and so on.
Can't we just build a Model.find() with multiple nested criteria?
I am starting to think that my collection structure is not good since it is hard to query. I thought it seemed logic but maybe no...
Thanks to anyone who can give a hint towards the solution
2 Answers 2
I think it will be difficult with a find but an aggregation pipeline could do the job.
What about this one:
db.leagues.aggregate([
{ $match: { _id: "693ae6dc320b2bb92635361e" } },
{
$project: {
cards: {
$filter: {
input: "$cards",
cond: { $eq: [ "$$this._id", "600056266" ] }
}
}
}
},
{ $unwind: "$cards" },
{
$project: {
users: {
$filter: {
input: "$cards.users",
cond: { $eq: [ "$$this._id", "EafXiJktdKSM45OcnniU4gpcGWc2" ] }
}
}
}
},
{ $unwind: "$users" },
{ $replaceWith: "$users.vote" }
])
You may set first stage to
{
$match: {
_id: "693ae6dc320b2bb92635361e",
"cards._id": "600056266",
"cards.users._id": "EafXiJktdKSM45OcnniU4gpcGWc2"
}
}
It may give better performance. It returns only the documents which will return a result, i.e. the following stages get only documents which let to an output.
cards filter you have as: "card" but you've used $$this._id. And your users filter is fine, since it defaults to $$this but in your playground example you called it user - in case you wanted to use a consistent style for both filters.db.collection.updateOne({ _id: "693ae6dc320b2bb92635361e" }, [{....}]). However, it cannot contain $unwind stage, see Update with an Aggregation Pipeline You can achieve it with $map, better open a new question for it.You can do this using a find query with a projection but it's harder to debug and a bit ugly. (I recommend using an aggregation pipeline like in Wernfried Domscheit's answer.)
- Use a regular
findwith the sub-objects' fields - Then a projection with nested
$map's &$filter's to get just the matching sub arrays for the card and user. Which are just aggregation stages being used in theprojectpart offind.
The result is a nested list and if you expect at most one match per document, then combine those with an arrayElemAt with 0 to only get the first one.
db.collection.find({
_id: "693ae6dc320b2bb92635361e",
"cards._id": "600056266",
"cards.users._id": "EafXiJktdKSM45OcnniU4gpcGWc2"
},
{
// all this is in the `project` part of the find query
_id: 0,
votes: {
$map: {
input: {
$filter: {
input: "$cards",
as: "c",
cond: { $eq: ["$$c._id", "600056266"] }
}
},
as: "resultCards",
in: {
$map: {
input: {
$filter: {
input: "$$resultCards.users",
as: "u",
cond: { $eq: ["$$u._id", "EafXiJktdKSM45OcnniU4gpcGWc2" ] }
}
},
as: "user",
in: "$$user.vote"
}
}
}
}
})
find() rather than aggregate(), however, $map and $filter are aggregation stages rather than Query Predicates.find - and that I don't recommend it; as mentioned at the beginning of my answer.