mtwallet
mtwallet

Reputation: 5086

Meteor Mongodb count fields in nested object

I am trying to create a dashboard where I show summaries of order data within the app. In this case I am simply wanting count the number of items in a given category in my Orders collection. My code so far looks like this:

Collection data

{
    "_id" : "a6wHiXxyM5DwSAsfq",
    "orderNumber" : 1234,
    "createdAt" : "11/01/2016, 14:43:49",
    "productsInOrder" : [
        {
            "category" : "ambient",
            "item" : 50818,
            "desc" : "Tasty Rubber Chicken",
            "quantity" : "44",
            "price" : "0.92",
            "lineprice" : "40.48",
            "_id" : "FFNxG8vujs6NGN69r"
        },
        {
            "category" : "frozen",
            "item" : 71390,
            "desc" : "Generic Granite Fish",
            "quantity" : "11",
            "price" : "1.00",
            "lineprice" : "11.00",
            "_id" : "LcRtpyLxkWyh39kkB"
        }
    ]
}
{
    "_id" : "PdpywXCvfew7qojmA",
    "orderNumber" : 1234,
    "createdAt" : "11/01/2016, 14:44:15",
    "productsInOrder" : [
        {
            "category" : "frozen",
            "item" : 71390,
            "desc" : "Generic Granite Fish",
            "quantity" : "44",
            "price" : "1.00",
            "lineprice" : "44.00",
            "_id" : "dAscx4R8pcBgbzoZs"
        },
        {
            "category" : "frozen",
            "item" : 66940,
            "desc" : "Gorgeous Granite Bike",
            "quantity" : "55",
            "price" : "4.21",
            "lineprice" : "231.55",
            "_id" : "xm3mFRmPmmdPxjfP9"
        },
        {
            "category" : "frozen",
            "item" : 96029,
            "desc" : "Gorgeous Plastic Fish",
            "quantity" : "1234",
            "price" : "4.39",
            "lineprice" : "5417.26",
            "_id" : "7u7SHnpTf7PWcrhGA"
        }
    ]
}
{
    "_id" : "xcHZ25qwvyDpDJtAZ",
    "orderNumber" : 1234,
    "createdAt" : "11/01/2016, 14:44:47",
    "productsInOrder" : [
        {
            "category" : "frozen",
            "item" : 31104,
            "desc" : "Handcrafted Rubber Keyboard",
            "quantity" : "11",
            "price" : "4.78",
            "lineprice" : "52.58",
            "_id" : "LMMwbKFEgnCbgCt9c"
        },
        {
            "category" : "frozen",
            "item" : 77832,
            "desc" : "Practical Rubber Shirt",
            "quantity" : "21",
            "price" : "0.62",
            "lineprice" : "13.02",
            "_id" : "63otkkXWGrTJkwEgX"
        },
        {
            "category" : "frozen",
            "item" : 66940,
            "desc" : "Gorgeous Granite Bike",
            "quantity" : "111",
            "price" : "4.21",
            "lineprice" : "467.31",
            "_id" : "rbPSujey8CFeMPjza"
        }
    ]
}

JS

So far I have tried:

Orders.find({ 'productsInOrder': ['ambient']}).count();
Orders.find({ productsInOrder: { category: 'ambient' }}).count();
Orders.find({ productsInOrder: { $all: [ 'frozen' ] }}).count();

I am having a hard time understanding Mongo queries when the data is nested in this manner. Please can you help point me in the right direction? Many thanks in advance.

* SOLUTION *

I have accomplished the desired result thanks to the contributions below. To make this work I created a method on the server as the query cannot be run on the client using an existing collection. This is done as follows:

Meteor.methods({
  'byCategory': function() {
    var result = Orders.aggregate([
        { "$unwind": "$productsInOrder" },
        {
            "$group": {
                "_id": null,
                "ambient_count": {
                    "$sum": {
                        "$cond": [ { "$eq": [ "$productsInOrder.category", "ambient" ] }, 1, 0 ]
                    }
                },
                "frozen_count": {
                    "$sum": {
                        "$cond": [ { "$eq": [ "$productsInOrder.category", "frozen" ] }, 1, 0 ]
                    }
                },
                "other_category_count": {
                    "$sum": {
                        "$cond": [ { "$eq": [ "$productsInOrder.category", "other_category" ] }, 1, 0 ]
                    }
                }
            }
        }
    ]);

    return result;
  }
})

and then on the client:

Meteor.call('byCategory', function( error, result ) {
  if( error ) {
    console.log( error.reason );
  } else {
    console.log( result[0].ambient_count );
    console.log( result[0].frozen_count );
    etc....
  }
});

Thanks and credit to @chridam and @Brett.

Upvotes: 3

Views: 892

Answers (2)

chridam
chridam

Reputation: 103355

An alternative approach is to use the aggregation framework. Consider the following aggregation pipeline which as the first stage of the aggregation pipeline, the $unwind operator denormalizes the productsInOrder array to output for each input document, n documents where n is the number of array elements. The next pipeline stage has the $group operator which groups all the documents into a single document and stores the counts for each category with the help of the $sum and $cond operators.

In Meteor, you can then use meteorhacks:aggregate package to implement the aggregation:

Add to your app with

meteor add meteorhacks:aggregate

Note, this only works on server side and there is no oberserving support or reactivity built in. Then simply use .aggregate function like below.

var coll = new Mongo.Collection('orders');
var pipeline = [
    { "$unwind": "$productsInOrder" },
    {
        "$group": {
            "_id": null, 
            "ambient_count": {
                "$sum": {
                    "$cond": [ { "$eq": [ "$productsInOrder.category", "ambient" ] }, 1, 0 ]
                }
            },
            "frozen_count": {
                "$sum": {
                    "$cond": [ { "$eq": [ "$productsInOrder.category", "frozen" ] }, 1, 0 ]
                }
            },
            "other_category_count": {
                "$sum": {
                    "$cond": [ { "$eq": [ "$productsInOrder.category", "other_category" ] }, 1, 0 ]
                }
            }
        }
    }       
];
var result = coll.aggregate(pipeline);

Running the same pipeline in mongo shell using the sample data will yield:

{
    "result" : [ 
        {
            "_id" : null,
            "ambient_count" : 1,
            "frozen_count" : 7,
            "other_category_count" : 0
        }
    ],
    "ok" : 1
}

You can access the native mongo collection and publish the aggregation results to the orders collection on the client side:

Meteor.publish('categoryCounts', function() {  
    var self = this,
        db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;

    orders = db.collection("orders").aggregate(pipeline, // Need to wrap the callback so it gets called in a Fiber.
        Meteor.bindEnvironment(
            function(err, result) {
                // Add each of the results to the subscription.
                _.each(result, function(e) {
                    self.added("orders", e._id, e);
                });
                self.ready();
            },
            function(error) {
                Meteor._debug( "Error doing aggregation: " + error);
            }
        )
    );
});

Upvotes: 3

Brett McLain
Brett McLain

Reputation: 2010

If you don't want to do this within Meteor, you will need to use mongo aggregation. Minimongo doesn't include aggregation though, so you will need this package to accomplish it:

https://docs.mongodb.org/manual/core/aggregation-introduction/

I only tested this in mongo itself, so you will have to adapt it to the way that the aggregation package does it:

db.orders.aggregate([
    {
        $unwind: "$productsInOrder"
    }, 
    {
        $match: {
            "productsInOrder.category": "frozen"
        }
    },
    {
        $group: {
            _id: null, 
            count: {
                $sum: 1
            }
         }
     }
]);

The first part is unwinding the collection. It will basically make an "order" entry for every instance of $productsInOrder. Once you have the array flattened out, we match on the category you care about; in this case, the "frozen" category. Next we group it up so we can count the number of documents returned. $group is simply constructing the final object that will be output from the query. You can modify this to be whatever you want, or you could group by productsInOrder.category and not even $match on "frozen".

Upvotes: 2

Related Questions