Home > Software design >  Mongoose to get the exact object from a nested array of objects
Mongoose to get the exact object from a nested array of objects

Time:03-13

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'}}
]);
  • Related