myusuf
myusuf

Reputation: 12260

Insert if not exists, else remove MongoDB

So I have a query in MongoDB (2.6.4) where I am trying to implement a simple upvote/downvote mechanism. When a user clicks upvote, I need to do the following:

If already upvoted by user, then remove upvote.

Else if not upvoted by user, then add upvote AND remove downvote if exists.

So far, my query formed (is incorrect) is:

db.collection.aggregate([
{
    $project: {
        "_id" : ObjectId("53e4d45c198d7811248cefca"),
        "upvote": {
           "$cond": 
            [
            {"$in": ["$upvote",1] },
            {"$pull": {"upvote" : 1}},
            {"$addToSet": {"upvote" : 1}, "$pull": {"downvote": 1}}
            ]
        }
    }
}
])

where '1' is the user id who is trying to upvote. Both upvote and downvote are arrays that contain userIds of those who have upvoted and downvoted, respectively.

For output of query, I just want a bool value: true if $cond evaluated to true, else false.

Upvotes: 3

Views: 3354

Answers (1)

Neil Lunn
Neil Lunn

Reputation: 151170

That's not a good way to implement up-votes and downvotes. Aside from the aggregation framework not being a mechanism for updating documents in any way, you seem to have gravitated towards thinking it may be a solution due to the logic you want to implement. But aggregate does not update.

What you want on your, well lets call it a "question" schema is a structure like this:

{
    "_id": ObjectId("53f51a844ffa9b02cf01c074"),
    "upvoted": [],
    "downvoted": [],
    "upvoteCount": 0,
    "downvoteCount": 0
}

That is something that can work well with atomic updates and actually give you some stateful information about the object at the same time.

For the "upvoted" and "downvoted" arrays, we are going to consider that the "users" voting have a similar unique ObjectId value. So what we are going to do is $push or $pull from either array and also "increment/decrement" the counter values along with each of those operations.

Here's how this works for an upvote:

db.questions.update(
    { 
        "_id": ObjectId("53f51a844ffa9b02cf01c074"),
        "upvoted": { "$ne": ObjectId("53f51c0a4ffa9b02cf01c075") }
        "downvoted": ObjectId("53f51c0a4ffa9b02cf01c075")
    },
    {         
        "$push": { "upvoted": ObjectId("53f51c0a4ffa9b02cf01c075") },
        "$inc": { "upvoteCount": 1, "downvoteCount": -1 },
        "$pull": { "downvoted": ObjectId("53f51c0a4ffa9b02cf01c075") },
    }
)

db.questions.update(
    { 
        "_id": ObjectId("53f51a844ffa9b02cf01c074"),
        "upvoted": { "$ne": ObjectId("53f51c0a4ffa9b02cf01c075") }
    },
    {
        "$push": { "upvoted": ObjectId("53f51c0a4ffa9b02cf01c075") },
        "$inc": { "upvoteCount": 1 },
    }
)

Actually that's two operations, which you could do with the Bulk operations API as well (probably the best way really) but it has a point to it. The first statement will only match a document where the current user has a "downvote" recorded in the array. As it, we already "pushed" that user id value to the "downvotes" array. If it is not there then no update is made. But you both push and pull from respective arrays and also "increment/decrement" the counter fields at the same time.

With the second statement which will only match something where the first did not, you make a fair assessment that now you don't need to touch "downvotes" and just handle the upvote fields. In both cases the safe thing to do is make sure that the main condition is the current user id value is not present in the "upvoted" array.

For downvotes the fields are just reversed:

db.questions.update(
    { 
        "_id": ObjectId("53f51a844ffa9b02cf01c074"),
        "downvoted": { "$ne": ObjectId("53f51c0a4ffa9b02cf01c075") }
        "upvoted": ObjectId("53f51c0a4ffa9b02cf01c075")
    },
    {         
        "$pull": { "upvoted": ObjectId("53f51c0a4ffa9b02cf01c075") },
        "$inc": { "upvoteCount": -1, "downvoteCount": 1 },
        "$push": { "downvoted": ObjectId("53f51c0a4ffa9b02cf01c075") },
    }
)

db.questions.update(
    { 
        "_id": ObjectId("53f51a844ffa9b02cf01c074"),
        "downvoted": { "$ne": ObjectId("53f51c0a4ffa9b02cf01c075") }
    },
    {
        "$push": { "downvoted": ObjectId("53f51c0a4ffa9b02cf01c075") },
        "$inc": { "downvoteCount": 1 },
    }
)

Naturally you can see the logical progression to simply cancelling any "upvote/downvote" for the user in question. Also you can be smart about it if you want and expose the information in your client to not only show if the current user have already "upvoted/downvoted" but also control click actions and eliminate unnecessary requests.

Upvotes: 7

Related Questions