问题
I have invoices, where each invoice contains a list of items. Each item has (among other things) the following fields:
- name
- qty
- total
And each invoice has (among other things) the fields:
- _id
- created
- items
The invoices live in a dedicated Mongo collection, called invoices.
I would like to get all the invoices containing the specified item, where for each invoice the following information needs to be returned:
- _id
- created
- qty (of the given item)
- total (of the given item)
Let us call the tuple <id, date, qty, total> as the invoice projection. Thus the result should be a list of invoice projections.
If an invoice lists the given item twice, then the respective invoice yields two projection instances. If an invoice does not list the given item at all, then this invoice is not present in the result.
Anyway, I am retrieving the required projections using the following Mongo aggregation pipeline:
pipeline = [
{$match: {'items.name': req.params.name}},
{$project: {created: 1, 'items.name': 1, 'items.qty': 1, 'items.total': 1}},
{$unwind: '$items'},
{$match: {'items.name': req.params.name}},
{$project: {created: 1, qty: '$items.qty', total: '$items.total'}}
],
The pipeline works like this:
- First match all the invoices having an item with the given name. There is a Mongo index on
items.name
, so this$match
is efficient. - An invoice is a large object, so strip all the fields leaving just the following structure:
{_id: ?, created: ?, items: [{name: ?, qty: ?, total: ?}, ..., {name: ?, qty: ?, total: ?}]}
- Unwind the
items
array, now we have a list of{_id: ?, created: ?, 'items.name': ?, 'items.qty': ?, 'items.total': ?}
objects. - Remove all the items which do not match the given name.
- Shape the final invoice projection.
The final list of invoice projections is then put through the following code:
function prepareResult(projections) {
var res = projections.reduce(function (acc, item) {
acc.itemCount += item.qty;
acc.total += item.total;
delete item.total;
return acc;
}, {itemCount: 0, total: 0});
res.items = projections;
return res;
}
It sums the qty
and total
fields over all of the invoice projections and returns a new object containg the projections as well as the calculated sums. The total
field is deleted from each and every invoice projection, since it is only the final sum that is needed.
My question - can I move the logic of the prepareResult
function into the Mongo aggregation pipeline?
回答1:
You need to add a $group step to the pipeline.
The _id of group will be what you are summing by (in this case a constant since you want a grand total). Since you want to retain the list of invoices, you can accumulate them into an array field via the $push operator. Sums for total and quantity would be handled with $sum.
{$group : { _id : 1,
Total : { $sum : "$total" },
ItemCount : { $sum : "$qty" },
Invoices : { $push : { id : "$_id", created : "$created" }}
} }
来源:https://stackoverflow.com/questions/17106193/using-mongo-aggregation-to-calculate-sum-of-values