I've a collection with many similar structured document, two of the document looks like

Input:

{ 
    "_id": ObjectId("525c22348771ebd7b179add8"), 
    "cust_id": "A1234", 
    "score": 500, 
    "status": "A"
    "clear": "No"
}

{ 
    "_id": ObjectId("525c22348771ebd7b179add9"), 
    "cust_id": "A1234", 
    "score": 1600, 
    "status": "B"
    "clear": "No"
}

By default the clear for all document is "No",

Req: I have to add the score of all documents with same cust_id, provided they belong to status "A" and status "B". If the score exceeds 2000 then I have to update the clear attribute to "Yes" for all of the document with the same cust_id.

Expected output:

{ 
    "_id": ObjectId("525c22348771ebd7b179add8"), 
    "cust_id": "A1234", 
    "score": 500, 
    "status": "A"
    "clear": "Yes"
}

{
    "_id": ObjectId("525c22348771ebd7b179add9"), 
    "cust_id": "A1234", 
    "score": 1600, 
    "status": "B"
    "clear": "Yes"
}

Yes because 1600+500 = 2100, and 2100 > 2000.


My Approach: I was only able to get the sum by aggregate function but failed at updating

db.aggregation.aggregate([
    {$match: {
        $or: [
            {status: 'A'},
            {status: 'B'}
        ]
    }},
    {$group: {
        _id: '$cust_id',
        total: {$sum: '$score'}
    }},
    {$match: {
        total: {$gt: 2000}
    }}
])

Please suggest me how do I proceed.

Solution 1

After a lot of trouble, experimenting mongo shell I've finally got a solution to my question.

Psudocode:

# To get the list of customer whose score is greater than 2000
cust_to_clear=db.col.aggregate(
    {$match:{$or:[{status:'A'},{status:'B'}]}},
    {$group:{_id:'$cust_id',total:{$sum:'$score'}}},
    {$match:{total:{$gt:500}}})

# To loop through the result fetched from above code and update the clear
cust_to_clear.result.forEach
(
   function(x)
   { 
     db.col.update({cust_id:x._id},{$set:{clear:'Yes'}},{multi:true}); 
   }
)

Please comment, if you have any different solution for the same question.

Solution 2

With Mongo 4.2 it is now possible to do this using update with aggregation pipeline. The example 2 has example how you do conditional updates:

db.runCommand(
   {
      update: "students",
      updates: [
         {
           q: { },
           u: [
                 { $set: { average : { $avg: "$tests" } } },
                 { $set: { grade: { $switch: {
                                       branches: [
                                           { case: { $gte: [ "$average", 90 ] }, then: "A" },
                                           { case: { $gte: [ "$average", 80 ] }, then: "B" },
                                           { case: { $gte: [ "$average", 70 ] }, then: "C" },
                                           { case: { $gte: [ "$average", 60 ] }, then: "D" }
                                       ],
                                       default: "F"
                 } } } }
           ],
           multi: true
         }
      ],
      ordered: false,
      writeConcern: { w: "majority", wtimeout: 5000 }
   }
)

Another example:

db.c.update({}, [
  {$set:{a:{$cond:{
    if: {},    // some condition
      then:{} ,   // val1
      else: {}    // val2 or "$$REMOVE" to not set the field or "$a" to leave existing value
  }}}}
]);

Solution 3

You need to do this in two steps:

  1. Identify customers (cust_id) with a total score greater than 200
  2. For each of these customers, set clear to Yes

You already have a good solution for the first part. The second part should be implemented as a separate update() calls to the database.

Psudocode:

# Get list of customers using the aggregation framework
cust_to_clear = db.col.aggregate(
    {$match:{$or:[{status:'A'},{status:'B'}]}},
    {$group:{_id:'$cust_id', total:{$sum:'$score'}}},
    {$match:{total:{$gt:2000}}}
    )

# Loop over customers and update "clear" to "yes"
for customer in cust_to_clear:
    id = customer[_id]
    db.col.update(
        {"_id": id},
        {"$set": {"clear": "Yes"}}
    )

This isn't ideal because you have to make a database call for every customer. If you need to do this kind of operation often, you might revise your schema to include the total score in each document. (This would have to be maintained by your application.) In this case, you could do the update with a single command:

db.col.update(
    {"total_score": {"$gt": 2000}},
    {"$set": {"clear": "Yes"}},
    {"multi": true}
    )

Solution 4

Short Answer: To avoid looping a Database query, just add $merge to the end and specify your collection like so:

db.aggregation.aggregate([
    {$match: {
        $or: [
            {status: 'A'},
            {status: 'B'}
        ]
    }},
    {$group: {
        _id: '$cust_id',
        total: {$sum: '$score'}
    }},
    {$match: {
        total: {$gt: 2000}
    }},
    { $merge: "<collection name here>"}
])

Elaboration: The current solution is looping through a database query, which is not good time efficiency wise and also a lot more code. Mitar's answer is not updating through an aggregation, but the opposite => using an aggregation within Mongo's update. If your wondering what is a pro in doing it this way, well you can use all of the aggregation pipeline as opposed to being restricted to only a few as specified in their documentation.

Here is an example of an aggregate that won't work with Mongo's update:

db.getCollection('foo').aggregate([
  { $addFields: {
      testField: {
        $in: [ "someValueInArray", '$arrayFieldInFoo']
      } 
  }},
  { $merge : "foo" }]
)

This will output the updated collection with a new test field that will be true if "someValueInArray" is in "arrayFieldInFoo" or false otherwise. This is NOT possible currently with Mongo.update since $in cannot be used inside update aggregate.

Update: Changed from $out to $merge since $out would only work if updating the entire collection as $out replaces entire collection with the result of the aggregate. $merge will only overrite if the aggregate matches a document (much safer).

Solution 5

In MongoDB 2.6., it will be possible to write the output of aggregation query, with the same command.

More information here : http://docs.mongodb.org/master/reference/operator/aggregation/out/

Solution 6

The solution which I found is using "$out"

*) e.g adding a field :

db.socios.aggregate(
    [
        {
            $lookup: {
                from: 'cuotas',
                localField: 'num_socio',
                foreignField: 'num_socio',
                as: 'cuotas'
            }
        },
        { 
            $addFields: { codigo_interno: 1001 } 
        },
        {
            $out: 'socios' //Collection to modify
        }
    ]
)

*) e.g modifying a field :

db.socios.aggregate(
        [
            {
                $lookup: {
                    from: 'cuotas',
                    localField: 'num_socio',
                    foreignField: 'num_socio',
                    as: 'cuotas'
                }
            },
            { 
                $set: { codigo_interno: 1001 } 
            },
            {
                $out: 'socios' //Collection to modify
            }
        ]
    )