Reputation: 9400
I have a mongodb (Mongoose entity) which represent a financial entry in my app:
var mongoose = require('mongoose');
var ObjectId = mongoose.Schema.Types.ObjectId;
var transactionSchema = mongoose.Schema({
description : String,
amount : { type:Number, min:0.01, required:true },
dateEntry : { type:Date, default:Date.now },
dateAdded : { type:Date, default:Date.now },
positive : { type:Boolean, default:false },
category : { type: ObjectId, ref: 'Category' },
user : { type: ObjectId, ref: 'User' }
});
module.exports = mongoose.model('Transaction', transactionSchema);
I would like to give a balance of my transactions: my first approach was making a sum of positive entries, then another sum of negative entries, and finally subtract the negative one from the positive one:
async.parallel({
totalPositive: function(callback) {
var matcher = baseSearch;
matcher.positive = true;
Transaction.aggregate(
{ $match: matcher },
{ $group: {_id:null,sum:{$sum: '$amount'}} },
function(err,result) {
callback(err,result);
}
);
},
totalNegative: function(callback) {
var matcher = baseSearch;
matcher.positive = false;
Transaction.aggregate(
{ $match: matcher },
{ $group: {_id:null,sum:{$sum: '$amount'}} },
function(err,result) {
callback(err,result);
}
);
}
}, function(err, results) {
if (err) {
return res.json({error:err.message});
}
results.balance = results.totalPositive[0].sum - results.totalNegative[0].sum;
res.json(results);
});
I wonder if I can save one query by having just one returning both the positive and negative sum.
Any ideas? Or is this the best solution? Thank you
Upvotes: 0
Views: 198
Reputation: 151092
You can do this with a conditional grouping and then just a final project to do the math:
Transaction.aggregate(
[
{ "$group": {
"_id": null,
"totalPositive": {
"$cond": [ "$positive", "$amount", 0 ]
},
"totalNegative": {
"$cond": [ "$positive", 0, "$amount" ]
}
}},
{ "$project": {
"totalBalance": { "$subtract": [ "$totalPositive", "$totalNegative" ] }
}}
],
function(err,result) {
}
)
So just evaluate whether to $sum
the "amount" depending on what "positive" has for either true/false
. Then in the following pipeline just use the $subtract
operator.
Better yet, just set the "sign" of the value to "sum" in a single group stage:
Transaction.aggregate(
[
{ "$group": {
"_id": null,
"totalBalance": {
"$sum": {
"$cond": [
"$positive",
"$amount",
{ "$subtract": [ 0, "$amount" ] }
]
}
}}
],
function(err,result) {
}
);
So there really is no need for the parallel queries when you can just do this in a single stroke.
The "meat" of each case is largely in the $cond
operator here, which acts as a ternary operator for evaluation. Link there, but if you are not familiar then this means a way of providing an "inline" if/then/else
evaluation. So there is a "test" as the first argument, and where true
the second argument is used in the return result, otherwise where false
the third argument is returned.
There are other samples available in the documentation at SQL to aggregation mapping which gives several common examples for those who are used to SQL, or are at least used to the "declarative" syntax for problem solving.
General reading on the aggregation framework is recommended, as it is essentially in SQL terms the differnce betweeen SELECT thisfield where anotherfield = "this"
to every other permuation of advanced SQL when compared to the basic .find()
method invocation.
Upvotes: 1