Mongoose populate virtual with sort and limit

2019-01-20 00:41发布

问题:

I am trying to write a mongoose query that retrieves a group of assets along with the most recent transactions for those assets. The transactions are in a separate collection from the assets.

To accomplish this, first I created a virtual array in the asset model to link the assets to the transactions.

schema.virtual('transactions', {
    ref: 'transaction',
    localField: '_id',
    foreignField: '_asset',
    justOne: false
})

Then I query using .populate in an node.js express controller (note the hard-coded "limit: 1" will become N at some point):

exports.getList = function (req, res) {
    Model
        .find({}, { __v: 0 })
        .populate({
            path: 'transactions',
            options: { sort: { 'created_at': -1}, limit: 1},
        })
        .lean()
        .exec(function (err, model) {
            if (err)
                res.send(err);
            res.json(model);
        });
  }

With 3 assets in my test system and a bunch of transactions for each one, it is returning old transactions for the first two assets and nothing for the third one.

When I remove the "limit:1" and return all transactions, it sorts properly and returns transactions for all three assets.

I believe I am running into this bug:

https://github.com/Automattic/mongoose/issues/4321

Any ideas on an elegant workaround?
Returning all transactions will not be a viable long-term solution.

回答1:

This is a curly issue indeed. The basics here is that .populate() and certainly not "populate with virtuals", is not designed to work like you are expecting here.

Issue Explained

In essence .populate() is essentially another query issued to MongoDB to retrieve related data. For this it basically issues a query with $in containing all of the "related field" values for the target to match on.

What is at the core of the "issue 4321" is that with such options as "sort" and "limit" the actual query that needs to be provided with such an $in argument is in fact an .aggregate() statement that would be able to "get the last n results grouped for each key". This is not actually what mongoose presently issues to MongoDB, nor is it really practical to group n items by available operations at this time.

You can work around that using .aggregate() manually, as demonstrated at the end of the provided listing, but of course only practically in a limited number of cases.

  // Get latest transactions for each master
  Transaction.aggregate([
    { '$match': {
      '_asset': {
        '$in': masters.map(m => m._id)
      }
    }},
    { '$sort': { '_asset': 1, 'createdAt': -1 } },
    { '$group': {
      '_id': '$_asset',
      'amount': { '$first': '$amount' },
      'createdAt': { '$first': '$createdAt' },
      'updatedAt': { '$first': '$updatedAt' },
      'did': { '$first': '$_id' }
    }},
    { '$project': {
      '_id': '$did',
      '_asset': '$_id',
      'amount': 1,
      'createdAt': 1,
      'updatedAt': 1
    }}
  ])

It's not great and not a really performant solution, but better than other similar alternatives. There is I believe though a still better way.

A Better Solution

For your case here, and I would suspect many similar cases, you don't want to include the "full" transactions list within the parent documents even as references due to the potential size of the arrays produced. This sort of "anti-pattern" is generally what "virtual populate" and indeed $lookup are designed to avoid.

In your particular use case of "get the latest transaction" though, neither of those are a viable solution. Since both would essentially require looking at "all" transactions and then only retrieving n results from them.

So the "latest" or indeed the "most recent" cases here actually falls back to "embedding" ( at least references ) at a "limited number" in order to provide a workable solution. So the proposal is to do exactly that, and keep a "recent" list of transactions within the parent itself. This gives you a document in the scenario included that looks like:

{
        "_id" : ObjectId("5959e34adf833e1451a32661"),
        "__v" : 0,
        "name" : "One",
        "recent" : [
                ObjectId("5959e34bdf833e1451a32676"),
                ObjectId("5959e34bdf833e1451a32674"),
                ObjectId("5959e34bdf833e1451a32672"),
                ObjectId("5959e34bdf833e1451a32670"),
                ObjectId("5959e34bdf833e1451a3266e")
        ]
}

Noting here that these are not "all" the related transactions but just the "most recent" ones. The point is to only keep a "small" list as appropriate to the purpose.

In this way, you can directly query the "parent" and just $slice off the array contained for the "recent" items. In the listing I do this as:

Master.find().select({ 'recent': { '$slice': 1 } })

That returns the "latest" entry from the array without any other query to the server. It's the "latest" in this case, because instead we "prepend" items to this array at the same time as we write to the "transactions" collection, which has everything:

Transaction.create({ _asset: master._id, amount: data.amount })
  .then(transaction =>
    Master.update(
      { _id: transaction._asset },
      { "$push": {
        "recent": {
          "$each": [transaction._id],
          "$position": 0,
          "$slice": 5
        }
      }}
    )

The key element there is the $push to the array in the parent which is modified with the $position to "preprend" at the beginning of the array so the "first" item is always the "latest" transaction to be added related to the parent.

Then the $slice modifier is used here to keep the "recent" array to a limit of n items only. So the "oldest" items will be "pushed off" the list as newer items are added.

There is then in addition another practical purpose here that when listing the "master" and "transactions" in a paging scenario, the first request can use the "recent" array directly. Then additional request for new "pages", can simply filter out the items contained within the "recent" array via $nin and use the regular .skip() and .limit() or alternate "range" paging practice to retrieve each "page" of results.

To demonstrate all concepts in full, see the listing and all the generated results below.

Demonstration listing:

const async = require('async'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.set('debug',true);
mongoose.Promise = global.Promise;

mongoose.connect('mongodb://localhost/prepend');

const transactionSchema = new Schema({
  _asset: { type: Schema.Types.ObjectId, ref: 'Master' },
  amount: Number
},{
  timestamps: {
    createdAt: 'createdAt'
  }
});


const Transaction = mongoose.model('Transaction', transactionSchema);

const masterSchema = new Schema({
  name: String,
  recent: [{ type: Schema.Types.ObjectId, ref: 'Transaction' }]
});

masterSchema.virtual('transactions', {
  ref: 'Transaction',
  localField: '_id',
  foreignField: '_asset',
  justOne: false
});


const Master = mongoose.model('Master', masterSchema);

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

async.series(
  [
    // Clean data
    (callback) =>
      async.each(mongoose.models,(model,callback) =>
        model.remove({},callback),callback),

    // Create Masters
    (callback) =>
      Master.insertMany(['One','Two'].map( name => ({ name })),callback),

    // Add 10 transactions to each master
    (callback) =>
      async.each(['One','Two'],(name,callback) =>
        async.eachSeries(
          Array.apply(null,Array(10)).map((e,i) => ({ name, amount: i+1 })),
          (data,callback) => {
            Master.findOne({ name: data.name })
              .then(master =>
                Transaction.create({ _asset: master._id, amount: data.amount })
              )
              .then(transaction =>
                Master.update(
                  { _id: transaction._asset },
                  { "$push": {
                    "recent": {
                      "$each": [transaction._id],
                      "$position": 0,
                      "$slice": 5
                    }
                  }}
                )
              )
              .then(res => callback())
              .catch(callback)
          },
          callback
        ),
      callback),

    // Show populated recent 1 entry only
    (callback) =>
      Master.find().select({ 'recent': { '$slice': 1 } })
        .populate('recent').exec((err,results) => {
        if (err) callback(err);
        log(results);
        callback();
      }),

    // Populate recent - page 1 then fetch next page
    (callback) =>
      async.waterfall(
        [
          (callback) =>
            Master.findOne({ name: 'One' }).populate('recent')
              .lean()
              .exec((err,master) => {
                if (err) callback(err);
                log(master);
                callback(null,{
                  _asset: master._id,
                  exclude: master.recent.map( r => r._id )
                });
              }),

          (options,callback) =>
            Transaction.find({
              _asset: options._asset,
              _id: { '$nin': options.exclude }
            }).sort({ 'createdAt': -1 }).limit(5)
            .exec((err,transactions) => {
              if (err) callback(err);
              log(transactions)
              callback();
            })

        ],
        callback
      ),

    // Issue 4321 - Fix - Manual populate with aggregate
    (callback) =>
      Master.find().select('-recent').exec()
        .then(masters => {
          // Get latest transactions for each master
          Transaction.aggregate([
            { '$match': {
              '_asset': {
                '$in': masters.map(m => m._id)
              }
            }},
            { '$sort': { '_asset': 1, 'createdAt': -1 } },
            { '$group': {
              '_id': '$_asset',
              'amount': { '$first': '$amount' },
              'createdAt': { '$first': '$createdAt' },
              'updatedAt': { '$first': '$updatedAt' },
              'did': { '$first': '$_id' }
            }},
            { '$project': {
              '_id': '$did',
              '_asset': '$_id',
              'amount': 1,
              'createdAt': 1,
              'updatedAt': 1
            }}
          ]).exec((err,transactions) => {
            // Map latest transactions to master
            masters = masters.map(
              m => Object.assign(
                m.toObject(),
                {
                  transactions: transactions.filter(
                    t => t._asset.toHexString() === m._id.toHexString()
                  )
                }
              )
            );

            log(masters);
            callback();
          })
        }).catch(callback)

  ],
  (err) => {
    if (err) throw err;
    mongoose.disconnect();
  }
);

Demonstration Output

Mongoose: transactions.remove({}, {})
Mongoose: masters.remove({}, {})
Mongoose: masters.insertMany([ { __v: 0, name: 'One', _id: 5959e34adf833e1451a32661, recent: [] }, { __v: 0, name: 'Two', _id: 5959e34adf833e1451a32662, recent: [] } ], null)
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:14 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:14 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 1, _id: ObjectId("5959e34adf833e1451a32663"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 1, _id: ObjectId("5959e34adf833e1451a32664"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34adf833e1451a32664") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34adf833e1451a32663") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 2, _id: ObjectId("5959e34bdf833e1451a32665"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 2, _id: ObjectId("5959e34bdf833e1451a32666"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32666") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32665") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 3, _id: ObjectId("5959e34bdf833e1451a32667"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 3, _id: ObjectId("5959e34bdf833e1451a32668"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32668") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32667") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 4, _id: ObjectId("5959e34bdf833e1451a32669"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 4, _id: ObjectId("5959e34bdf833e1451a3266a"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a3266a") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32669") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 5, _id: ObjectId("5959e34bdf833e1451a3266b"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 5, _id: ObjectId("5959e34bdf833e1451a3266c"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a3266c") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a3266b") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 6, _id: ObjectId("5959e34bdf833e1451a3266d"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 6, _id: ObjectId("5959e34bdf833e1451a3266e"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a3266e") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a3266d") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 7, _id: ObjectId("5959e34bdf833e1451a3266f"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 7, _id: ObjectId("5959e34bdf833e1451a32670"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32670") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a3266f") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 8, _id: ObjectId("5959e34bdf833e1451a32671"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 8, _id: ObjectId("5959e34bdf833e1451a32672"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32672") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32671") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 9, _id: ObjectId("5959e34bdf833e1451a32673"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 9, _id: ObjectId("5959e34bdf833e1451a32674"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32674") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32673") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: masters.findOne({ name: 'Two' }, { fields: {} })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32662"), amount: 10, _id: ObjectId("5959e34bdf833e1451a32675"), __v: 0 })
Mongoose: transactions.insert({ updatedAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), createdAt: new Date("Mon, 03 Jul 2017 06:25:15 GMT"), _asset: ObjectId("5959e34adf833e1451a32661"), amount: 10, _id: ObjectId("5959e34bdf833e1451a32676"), __v: 0 })
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32661") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32676") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.update({ _id: ObjectId("5959e34adf833e1451a32662") }, { '$push': { recent: { '$each': [ ObjectId("5959e34bdf833e1451a32675") ], '$slice': 5, '$position': 0 } } }, {})
Mongoose: masters.find({}, { fields: { recent: { '$slice': 1 } } })
Mongoose: transactions.find({ _id: { '$in': [ ObjectId("5959e34bdf833e1451a32676"), ObjectId("5959e34bdf833e1451a32675") ] } }, { fields: {} })
[
  {
    "_id": "5959e34adf833e1451a32661",
    "__v": 0,
    "name": "One",
    "recent": [
      {
        "_id": "5959e34bdf833e1451a32676",
        "updatedAt": "2017-07-03T06:25:15.282Z",
        "createdAt": "2017-07-03T06:25:15.282Z",
        "_asset": "5959e34adf833e1451a32661",
        "amount": 10,
        "__v": 0
      }
    ]
  },
  {
    "_id": "5959e34adf833e1451a32662",
    "__v": 0,
    "name": "Two",
    "recent": [
      {
        "_id": "5959e34bdf833e1451a32675",
        "updatedAt": "2017-07-03T06:25:15.280Z",
        "createdAt": "2017-07-03T06:25:15.280Z",
        "_asset": "5959e34adf833e1451a32662",
        "amount": 10,
        "__v": 0
      }
    ]
  }
]
Mongoose: masters.findOne({ name: 'One' }, { fields: {} })
Mongoose: transactions.find({ _id: { '$in': [ ObjectId("5959e34bdf833e1451a32676"), ObjectId("5959e34bdf833e1451a32674"), ObjectId("5959e34bdf833e1451a32672"), ObjectId("5959e34bdf833e1451a32670"), ObjectId("5959e34bdf833e1451a3266e") ] } }, { fields: {} })
{
  "_id": "5959e34adf833e1451a32661",
  "__v": 0,
  "name": "One",
  "recent": [
    {
      "_id": "5959e34bdf833e1451a32676",
      "updatedAt": "2017-07-03T06:25:15.282Z",
      "createdAt": "2017-07-03T06:25:15.282Z",
      "_asset": "5959e34adf833e1451a32661",
      "amount": 10,
      "__v": 0
    },
    {
      "_id": "5959e34bdf833e1451a32674",
      "updatedAt": "2017-07-03T06:25:15.264Z",
      "createdAt": "2017-07-03T06:25:15.264Z",
      "_asset": "5959e34adf833e1451a32661",
      "amount": 9,
      "__v": 0
    },
    {
      "_id": "5959e34bdf833e1451a32672",
      "updatedAt": "2017-07-03T06:25:15.216Z",
      "createdAt": "2017-07-03T06:25:15.216Z",
      "_asset": "5959e34adf833e1451a32661",
      "amount": 8,
      "__v": 0
    },
    {
      "_id": "5959e34bdf833e1451a32670",
      "updatedAt": "2017-07-03T06:25:15.195Z",
      "createdAt": "2017-07-03T06:25:15.195Z",
      "_asset": "5959e34adf833e1451a32661",
      "amount": 7,
      "__v": 0
    },
    {
      "_id": "5959e34bdf833e1451a3266e",
      "updatedAt": "2017-07-03T06:25:15.180Z",
      "createdAt": "2017-07-03T06:25:15.180Z",
      "_asset": "5959e34adf833e1451a32661",
      "amount": 6,
      "__v": 0
    }
  ]
}
Mongoose: transactions.find({ _id: { '$nin': [ ObjectId("5959e34bdf833e1451a32676"), ObjectId("5959e34bdf833e1451a32674"), ObjectId("5959e34bdf833e1451a32672"), ObjectId("5959e34bdf833e1451a32670"), ObjectId("5959e34bdf833e1451a3266e") ] }, _asset: ObjectId("5959e34adf833e1451a32661") }, { sort: { createdAt: -1 }, limit: 5, fields: {} })
[
  {
    "_id": "5959e34bdf833e1451a3266c",
    "updatedAt": "2017-07-03T06:25:15.164Z",
    "createdAt": "2017-07-03T06:25:15.164Z",
    "_asset": "5959e34adf833e1451a32661",
    "amount": 5,
    "__v": 0
  },
  {
    "_id": "5959e34bdf833e1451a3266a",
    "updatedAt": "2017-07-03T06:25:15.135Z",
    "createdAt": "2017-07-03T06:25:15.135Z",
    "_asset": "5959e34adf833e1451a32661",
    "amount": 4,
    "__v": 0
  },
  {
    "_id": "5959e34bdf833e1451a32668",
    "updatedAt": "2017-07-03T06:25:15.080Z",
    "createdAt": "2017-07-03T06:25:15.080Z",
    "_asset": "5959e34adf833e1451a32661",
    "amount": 3,
    "__v": 0
  },
  {
    "_id": "5959e34bdf833e1451a32666",
    "updatedAt": "2017-07-03T06:25:15.039Z",
    "createdAt": "2017-07-03T06:25:15.039Z",
    "_asset": "5959e34adf833e1451a32661",
    "amount": 2,
    "__v": 0
  },
  {
    "_id": "5959e34adf833e1451a32664",
    "updatedAt": "2017-07-03T06:25:15.009Z",
    "createdAt": "2017-07-03T06:25:15.009Z",
    "_asset": "5959e34adf833e1451a32661",
    "amount": 1,
    "__v": 0
  }
]
Mongoose: masters.find({}, { fields: { recent: 0 } })
Mongoose: transactions.aggregate([ { '$match': { _asset: { '$in': [ 5959e34adf833e1451a32661, 5959e34adf833e1451a32662 ] } } }, { '$sort': { _asset: 1, createdAt: -1 } }, { '$group': { _id: '$_asset', amount: { '$first': '$amount' }, createdAt: { '$first': '$createdAt' }, updatedAt: { '$first': '$updatedAt' }, did: { '$first': '$_id' } } }, { '$project': { _id: '$did', _asset: '$_id', amount: 1, createdAt: 1, updatedAt: 1 } } ], {})
[
  {
    "_id": "5959e34adf833e1451a32661",
    "__v": 0,
    "name": "One",
    "transactions": [
      {
        "amount": 10,
        "createdAt": "2017-07-03T06:25:15.282Z",
        "updatedAt": "2017-07-03T06:25:15.282Z",
        "_id": "5959e34bdf833e1451a32676",
        "_asset": "5959e34adf833e1451a32661"
      }
    ]
  },
  {
    "_id": "5959e34adf833e1451a32662",
    "__v": 0,
    "name": "Two",
    "transactions": [
      {
        "amount": 10,
        "createdAt": "2017-07-03T06:25:15.280Z",
        "updatedAt": "2017-07-03T06:25:15.280Z",
        "_id": "5959e34bdf833e1451a32675",
        "_asset": "5959e34adf833e1451a32662"
      }
    ]
  }
]