Home > Back-end >  Use find and set in aggregation pipeline to populate value in nested document based on list of ids
Use find and set in aggregation pipeline to populate value in nested document based on list of ids

Time:02-03

In this example doc, _id and myOtherId are Object Ids.

{
   _id: 1
   name: "Joe"
   myAreas: [
             {_id: 1a, isActive: false, myOtherId: 1a1, area: "Gaming"},
             {_id: 2a, isActive: false, myOtherId: 1a2, area: "Fishing"},
             {_id: 3a, isActive: false, myOtherId: 1a3, area: "Cooking"},
            ]
}

Let say I have a list of ids to change isActive to true.

activeIds = ['1a1', '1a3']

I need to create a way to update the nested documents if myOtherId is in the list of activeIds. If it isn't on that list, it should change the value to false (assuming in the future I will set different areas as active.

This is what I have so far but it just converts everything to false. Im using the aggregation pipeline approach. I know I'm messing up on the $ operator, but cant seem to figure out the right syntax to make this work. I also tried converting the activeIds list to a list of Object Ids like : activeIds = [ObjectId('1a1'), ObjectId('1a3')]

await User.findByIdAndUpdate(1,
[
  {
    $set: {
           "myAreas.isActive": {
               $in: ["myAreas.$.myOtherId", activeIds],
                },
              },
            },
  ],
          { multi: true, new: true }
        );

Any idea where I am going wrong? Is this even on the right track or am I completely off?

If I rewrite like this, it works but the issue here is it just updates the activeIds isActive prop to true and doesnt set the rest of the ids not on the list to false.

await User.updateMany(
  { _id: 1 },
  {
   $set: {
          "myAreas.$[elem].isActive": true,
         },
  },
  {
   arrayFilters: [{ "elem.myOtherId": { $in: activeIds} }],
   multi: true,
   new: true,
  }
);

CodePudding user response:

Maybe you are looking for something like this with two arrayFilters:

   db.collection.update({},
    {
    $set: {
     "myAreas.$[x].isActive": "TRUE",
     "myAreas.$[y].isActive": "FALSE"
     }
    },
    {
     arrayFilters: [
     {
      "x.myOtherId": {
       $not: {
         $in: [
             "1a1",
             "1a2"
              ]
        }
      }
    },
     {
      "y.myOtherId": {
        $in: [
           "1a1",
           "1a2"
             ]
          }
        }
      ],
      multi: true
      })

Explained: You add two arrayFilters x & y , so the query will be applied explicitely for any of the cases :

$in -> TRUE

$not $in -> FALSE

from the provided ids , in the example array list is ["1a1","1a2"].

playground

CodePudding user response:

An alternative is to use the $merge stage. As of v4.4 (Aug 2021), $merge can output back onto the same collection as the input. This means we can use it like a giant update stage. Consider this slightly larger input set:

var r = [
{
    name: "Joe",
   myAreas: [
       {_id: '1a', isActive: false, myOtherId: '1a1', area: "Gaming"},
       {_id: '2a', isActive: false, myOtherId: '1a2', area: "Fishing"},
       {_id: '3a', isActive: false, myOtherId: '1a3', area: "Cooking"}
   ]
},
{
    name: "Bob",
   myAreas: [
       {_id: '3a', isActive: false, myOtherId: '1a3', area: "Cooking"},
       {_id: '4a', isActive: false, myOtherId: '1a4', area: "Scuba"}
   ]
},
{
    name: "Tim",
   myAreas: [
       {_id: '5a', isActive: false, myOtherId: '1a5', area: "X1"},
       {_id: '6a', isActive: false, myOtherId: '1a6', area: "X2"}
   ]
}
];

Here is the pipeline:

db.foo.aggregate([
    // Only pick up those docs that contain at least 1 activeIds.                                   
    // This will not pinpoint the position -- it will just                                          
    // eliminate docs that need no updating:                                                        
    {$match: {"myAreas.myOtherId": {$in: activeIds}} },

    // Use map to run over myAreas.  If myOtherId is in the activeIds,                              
    // then set isActive to true.  This is how we say  doc[field] = true                            
    // in a map.  We take the doc ($$z) and overlay a new field.value                               
    // on top of it.                                                                                
    // Notice that we use $project; we are cutting down the doc to                                  
    // just myAreas (and _id, which always comes along...                                           
    {$project: {myAreas: {$map: {
        input: "$myAreas",
        as: "z",
            in: {$cond: [
                {$in: [ "$$z.myOtherId", activeIds ]}, // if                                        
                {$mergeObjects: ["$$z", {isActive:true}]}, // then overlay true                     
                "$$z" // else do nothing; leave the entry alone.                                    
            ]
                }
        }}
    }}

    // Now, push back onto the input collection based on _id (fast).  The                           
    // whenMatched = "merge" means the behavior is similar to $mergeObjects.                        
    // Just the myAreas array (which is the only non _id field in the pipeline)                     
    // will be merged into the existing doc:                                                       
    ,{$merge: {
        into: "foo",
        on: [ "_id" ],
        whenMatched: "merge",
        whenNotMatched: "fail"
    }}
]);

This approach is both clear and straightforward and allows for plenty of other conditional updates. It is less efficient when array fields like myAreas are very long and/or very big and only a very small subset of the elements have any changes because the whole field is merged back onto the DB.

  •  Tags:  
  • Related