1

I have the following document in a Mongo database:

{
    _id: 1,
    question: "Blue or red?",
    __v: 0,
    votes: [9, 5]
}

I want to, in the back-end, get the total of votes, 14, (because 9 + 5 = 14) and append a new property on that document dynamically. So in the end, it'll look like this:

{
    _id: 1,
    question: "Blue or red?",
    __v: 0,
    votes: [9, 5],
    totalVotes: 14
}

I researched how to do this, and found words like: Aggregate, unwind, etc. I don't think those will help me because Aggregating in MongoDB is comparing the properties of documents, and I only want to modify the innards of a singular document relative to the properties of itself.

This was my "attempt" to do so:

var getTotal = function(arr){
   var total = 0;
   for(var i in arr) { total += arr[i]; }
   return total;
 };

Model.update({ $set: { totalVotes: getTotal(votes) }}, function(){
  console.log('Updated.');
});

Obviously votes is not defined, and this will result in an error. How would I tell it to get the votes from the Model, and pass it in the getTotal() function?

Here's the schema of the Model, btw:

question: {type: String, required: true},
votes: [Number],
totalVotes: Number
Matthew
  • 2,158
  • 7
  • 30
  • 52

3 Answers3

3

supposing your Schema is ExampleSchema

var ExampleSchema = new Schema({
    question: {type: String, required: true},
    votes: [Number]
}, {
  toObject: {
     virtuals: true
  },
  toJSON: {
     virtuals: true 
  }
});

this will allow virtuals when querying for documents in mongoDB

Now for the implementation of virtual simple after declaring the schema.

ExampleSchema
.virtual('totalVotes')
.get(function () {
  return this.votes.reduce(function(currentValue, previousValue){
     return (currentValue + previousValue);
  });
});
Minato
  • 4,383
  • 1
  • 22
  • 28
2

You can also use .aggregate() method to do this:

From MongoDB 3.2 you can use the $sum operator in the $project stage. More info in the documentation.

Model.aggregate(
    [
        { '$match': { '_id': 1 } },
        { "$project": { 
            "totalVotes": { "$sum": "$votes" } 
        }}
    ], 
    function(err, result) {
        Model.update(
            { '_id': result[0]['_id'] },  
            { '$set': { 'totalVotes': result[0]['totalVotes'] }},
            function(){
                console.log('Updated.');
            }
        );
    }
)

In previous version you need to first $unwind the "votes" array.

Model.aggregate(
    [
        { "$match": { "_id": 1 } },
        { "$unwind": "$votes" }, 
        { "$group": { 
            "_id": "$_id",  
            "totalVotes": { "$sum": "$votes" } 
        }}
    ],
    function(err, result) {
        Model.update(
            { '_id': result[0]['_id'] },  
            { '$set': { 'totalVotes': result[0]['totalVotes'] }},
            function(){
                console.log('Updated.');
            }
        );
    }
)
styvane
  • 59,869
  • 19
  • 150
  • 156
0

@Minato said I should try something called virtuals. I did, and the outcome was good, except for one thing...

Here is the document:

 {
    _id: 1,
    question: "Blue or red?",
    __v: 0,
    votes: [9, 5]
}

I want to add an attribute called totalVotes with the value of 14. (9 + 5 = 14)

So, consider this schema:

 var PollSchema = new Schema(
{
    question: {type: String, required: true},
    votes: [Number]
}, 
{
  toObject: {
    virtuals: true
  },
  toJSON: {
    virtuals: true 
  }
}
);

Now, with that schema, I add the virtual attribute:

 PollSchema.virtual('totalVotes').get(function() {
   var total =  getTotal(this.votes);
   return total;
 });

And, I get this:

 {
    _id: 1,
    question: "Blue or red?",
    __v: 0,
    votes: [9, 5],
    totalVotes: "14[object Object]"
}

Now, apparently, the output is JSON so the "[object Object]" is apart of the string. To remove it, I just use the parseInt() method...

PollSchema.virtual('totalVotes').get(function() {
   var total =  getTotal(this.votes);
  return parseInt(total);
});

And the output is EXACTLY what I've been wanting all along..

 {
    _id: 1,
    question: "Blue or red?",
    __v: 0,
    votes: [9, 5],
    totalVotes: 14
}
Matthew
  • 2,158
  • 7
  • 30
  • 52
  • Good.. but you should use `Array` prototype methods ... :P – Minato Dec 26 '15 at 10:59
  • See my answer if you want to get the idea.. :P – Minato Dec 26 '15 at 11:00
  • You're getting that garbage in `totalVotes` because `votes` is a `MongooseDocumentArray`, not a plain JS array and your `for...in` is picking up other enumerable properties besides the indexes. Which is why it's typically a [bad idea](http://stackoverflow.com/questions/500504/why-is-using-for-in-with-array-iteration-such-a-bad-idea) to use `for...in` to iterate over arrays. – JohnnyHK Dec 26 '15 at 15:37
  • @JohnnyHK Thanks for the tip. I just got that `for...in` loop when I looked for how to get the sum of an array. I just now researched the `reduce()` array method, and knowing how simple it is to process an array from left to right and reducing it down to a single number, that seems to be a much better approach. – Matthew Dec 26 '15 at 18:58