1

I use MongoDB native driver in my NodeJS application.

I have a shifts collection in my database that I need to update. Sample docs in my shifts collection

{
    "_id" : ObjectId("588425105560bd2ba0065fa4"),
    "from" : ISODate("2017-01-23T03:20:00.000Z"),
    "to" : ISODate("2017-01-23T06:20:00.000Z"),
    "jobId" : ObjectId("586efda790541421b0432897"),
    "hourlyRate" : 15
}

{
    "_id" : ObjectId("588425105560bd2ba0065fa5"),
    "from" : ISODate("2017-01-25T03:20:00.000Z"),
    "to" : ISODate("2017-01-25T06:20:00.000Z"),
    "jobId" : ObjectId("586efda790541421b0432897"),
    "hourlyRate" : 15
}

What I need to do is the following -

Update the hourlyRate of all docs that meet the conditions:

  • match the jobId (that is easy)
  • set hourlyRate = 20 if from is a Weekday
  • set hourlyRate = 25 if from is a Saturday
  • set hourlyRate = 30 if from is a Sunday

I would want to do it in a single query as far as possible.

My solution so far:

Use switch case and determine the type of day using $dayOfWeek from Date aggregation function. However, I am not able to combine switch with updateMany.

Any help would be appreciated.

jerry
  • 377
  • 3
  • 17

1 Answers1

1

You could run the following aggregation pipeline with special operators at your disposal like $switch which is new in MongoDB Server 3.4 and above:

MongoDB Server 3.4:

db.collection('shifts').aggregate([
    {
        "$match": {
            "jobId": ObjectId(job._id),
            "from": { "$gte": new Date() }
        }
    },
    {
        "$project": {
            "hourlyRate": {
                "$switch": {
                    "branches": [
                        {
                            "case": { 
                                "$not": { 
                                    "$in": [
                                        { "$dayOfWeek": "$from" }, 
                                        [1, 7] 
                                    ] 
                                } 
                            }, 
                            "then": 20 
                        },
                        { 
                            "case": { 
                                "$eq": [
                                    { "$dayOfWeek": "$from" }, 
                                    7
                                ] 
                            }, 
                            "then": 25 
                        },
                        { 
                            "case": { 
                                "$eq": [
                                    { "$dayOfWeek": "$from" }, 
                                    1 
                                ] 
                            }, 
                            "then": 30 
                        }
                    ]
                }   
            }               
        }
    }       
], function(err, docs) {
    var ops = [],
        counter = 0;

    docs.forEach(function(doc) {
        ops.push({
            "updateOne": {
                "filter": { "_id": doc._id },
                "update": { "$set": { "hourlyRate": doc.hourlyRate } }
            }
        });
        counter++;

        if (counter % 500 === 0) {
            db.collection('shifts').bulkWrite(ops, function(err, r) {
                // do something with result
            });
            ops = [];
        }
    })

    if (counter % 500 !== 0) {
        db.collection('shifts').bulkWrite(ops, function(err, r) {
            // do something with result
        }
    }       
});

MongoDB Server 3.2

 db.collection('shifts').aggregate([
    {
        "$match": {
            "jobId": ObjectId(job._id),
            "from": { "$gte": new Date() }
        }
    },
    {
        "$project": {
            "hourlyRate": {
                "$cond": [
                    {
                        "$not": { 
                            "$setIsSubset": [
                                [{ "$dayOfWeek": "$from" }], 
                                [1, 7] 
                            ] 
                        } 
                    }, 20,                                
                    { 
                        "$cond": [
                            { "$eq": [
                                { "$dayOfWeek": "$from" }, 
                                7
                            ] },
                            25,
                            { 
                                "$cond": [ 
                                    { "$eq": [
                                        { "$dayOfWeek": "$from" }, 
                                        1 
                                    ] },
                                    30,
                                    "$hourlyRate"
                                ]
                            }
                        ]
                    }                   
                ]                   
            }               
        }
    }
], function(err, docs) {
    var ops = [],
        counter = 0;

    docs.forEach(function(doc) {
        ops.push({
            "updateOne": {
                "filter": { "_id": doc._id },
                "update": { "$set": { "hourlyRate": doc.hourlyRate } }
            }
        });
        counter++;

        if (counter % 500 === 0) {
            db.collection('shifts').bulkWrite(ops, function(err, r) {
                // do something with result
            });
            ops = [];
        }
    })

    if (counter % 500 !== 0) {
        db.collection('shifts').bulkWrite(ops, function(err, r) {
            // do something with result
        }
    }       
})
chridam
  • 100,957
  • 23
  • 236
  • 235
  • I was just testing it again and I realised that executing this query overwrites all docs in the shifts collection. For eg, if I execute the above query with $match criteria for jobId = 1, then it does update all docs correctly for jobId = 1 but at the same time all other docs are deleted! Can you please check for that? Thanks – jerry Jan 23 '17 at 06:27
  • Please check my question to see how I use the $match in the aggregation. I am using the $addFields version of your answer (the one intended for mongo 3.4) – jerry Jan 23 '17 at 06:29
  • I think this is how $out works. "If the collection specified by the $out operation already exists, then upon completion of the aggregation, the $out stage atomically replaces the existing collection with the new results collection". But then I don't want to replace my entire collection since I am only updating $hourlyRate for specific jobIds and not all docs. – jerry Jan 23 '17 at 06:39
  • My bad, completely missed the `$match` filter part hence the "full" collection update via the `$out`. Nonetheless, I have updated my answer with another approach which uses the bulk API, please check. – chridam Jan 23 '17 at 06:52
  • Thanks for that. I have a query - Does bulkWrite actually perform all updates at once? If not, what will be the performance of this operation? And any specific reason for performing 500 operations at once? – jerry Jan 23 '17 at 07:03
  • Yes, `bulkWrite()` is more efficient than sending each update request to the server, it eliminates the "back and forth" which can be very expensive in terms of IO by sending the update operations in groups. The reason for choosing 500 is to ensure that the sum of the associated document from the update document is less than or equal to the maximum BSON document size even though there is no there is no guarantee using the default 1000 operations requests will fit under the 16MB BSON limit. The Bulk() operations in the mongo shell and comparable methods in the drivers do not have this limit. – chridam Jan 23 '17 at 07:30
  • More details [here](https://docs.mongodb.com/manual/reference/limits/#Write-Command-Operation-Limit-Size) – chridam Jan 23 '17 at 07:33
  • 1
    Great. I am going to accept this answer. Just a minor correction you need to do.. In ops.push, you need to use doc._id and not doc.id.. The entire solution works fine :). Can you please edit? – jerry Jan 23 '17 at 07:55
  • Can you please help me with this question http://stackoverflow.com/questions/41819420/groupby-multiple-columns-in-mongodb – jerry Jan 24 '17 at 03:15