I want to retrieve the exact object from an array of nested objects using a mongoose query. I have tried some queries using $elemMatch but not getting the exact object I am targeting rather I get the array of objects back.
My BSON is
{
"_id": {
"$oid": "622bcb7a1091ddd45201258e"
},
"type": "Humans",
"person": [
{
"name": "Name Person 1",
"visited": [],
"_id": {
"$oid": "622bdjnmyi30e62d6d166ebd"
}
},
{
"name": "Name Person 2",
"visited": [
{
"country": "Country 1",
"year": "2022",
"id": "zuPks8cv3n"
}
],
"_id": {
"$oid": "622bdopmLks0e62d6d166ebe"
}
},
{
"name": "Name Person 3",
"_id": {
"$oid": "622bdpo8bnj0e62d6d166ebf"
},
"visited": [
{
"country": "Country 3",
"year": "2029",
"id": "l2Opx489xb"
},
{
"country": "Country 4",
"year": "2002",
"id": "s09zbHYjIp"
}
]
},
{
"name": "Name Person 4",
"visited": [],
"_id": {
"$oid": "622bdb9eio0sbt2d6d166ec0"
}
}
],
"__v": {
"$numberInt": "0"
}
}
I am targeting "Name Person 3" with the array of objects "visited" and object with id "l2Opx489xb".
My expected result is:
{
"country": "Country 3",
"year": "2029",
"id": "l2Opx489xb"
}
CodePudding user response:
This is a variation of the common problem where an attempt to match a single item in an array yields the entire array. The issue is not "matching" because the match is correct; it is a question of projection. After we find a match, we only wish to project specific items in the array or the rest of the doc in general.
Here is a solution that has a single stage and no $unwind
. In general, between $filter
, $map
, and $reduce
it is possible to extract data from single documents without $unwind
, which can be expensive. Read the comments from "inside-out" starting with #1.
db.foo.aggregate([
{$replaceRoot: {newRoot: // #7 ...and make this the root object
{$first: // #6 like #2, turn the array of 1 into a single object.
{$filter: { // #4 ... and now we filter the 'visited' array...
input: {$let: {
vars: {qq: {$first: // #2 $filter will yield an array of 0 or 1;
// use $first to turn into one object
// #1 Find Name Person 3
{$filter: {
input: '$person',
cond: {$eq:['$$this.name','Name Person 3']}
}}
}},
// #3 We wish we could say $first.visited in #2 but we cannot
// so we use $let and the vars setup to allow us to get to
// the inner array 'visited':
in: '$$qq.visited'}
},
cond: {$eq:['$$this.id','l2Opx489xb']} // #5 to match target id
}}
}
}}
]);
{ "country" : "Country 3", "year" : "2029", "id" : "l2Opx489xb" }
NOTE: $first
arrived in v4.4. For earlier version, instead of
{$first: <expression that yields array>}
use this instead:
{$arrayElemAt: [ <expression that yields array>, 0]
$first
is a little cleaner because you do not have the ,0
"dangling" at the end of a complex expression.
Below is the "expanded" version plus an extra check against $X
evaluating to null
(if Name Person 3
or target id is not present) because null
cannot be passed to $replaceRoot
:
db.foo.aggregate([
{$project: {
X: {$first: {$filter: {
input: '$person',
cond: {$eq:['$$this.name','Name Person 3']}
}} }
}}
,{$project: {
X: {$first: {$filter: {
input: '$X.visited',
cond: {$eq:['$$this.id','l2Opx489xb']}
}} }
}}
,{$match: {X: {$ne: null}} }
,{$replaceRoot: {newRoot: '$X'}}
]);