8

I have an article schema that has a subdocument comments which contains all the comments i got for this particular article.

What i want to do is select an article by id, populate its author field and also the author field in comments. Then sort the comments subdocument by date.

the article schema:

var articleSchema = new Schema({
  title: { type: String, default: '', trim: true },
  body: { type: String, default: '', trim: true },
  author: { type: Schema.ObjectId, ref: 'User' },
  comments: [{
    body: { type: String, default: '' },
    author: { type: Schema.ObjectId, ref: 'User' },
    created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
  }],
  tags: { type: [], get: getTags, set: setTags },
  image: {
    cdnUri: String,
    files: []
  },
  created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
});

static method on article schema: (i would love to sort the comments here, can i do that?)

  load: function (id, cb) {
    this.findOne({ _id: id })
      .populate('author', 'email profile')
      .populate('comments.author')
      .exec(cb);
  },

I have to sort it elsewhere:

exports.load = function (req, res, next, id) {
  var User = require('../models/User');

  Article.load(id, function (err, article) {
    var sorted = article.toObject({ getters: true });
    sorted.comments = _.sortBy(sorted.comments, 'created_at').reverse();

    req.article = sorted;
    next();
  });
};

I call toObject to convert the document to javascript object, i can keep my getters / virtuals, but what about methods??

Anyways, i do the sorting logic on the plain object and done.

I am quite sure there is a lot better way of doing this, please let me know.

aegyed
  • 1,320
  • 3
  • 20
  • 39

3 Answers3

9

I could have written this out as a few things, but on consideration "getting the mongoose objects back" seems to be the main consideration.

So there are various things you "could" do. But since you are "populating references" into an Object and then wanting to alter the order of objects in an array there really is only one way to fix this once and for all.

Fix the data in order as you create it


If you want your "comments" array sorted by the date they are "created_at" this even breaks down into multiple possibilities:

  1. It "should" have been added to in "insertion" order, so the "latest" is last as you note, but you can also "modify" this in recent ( past couple of years now ) versions of MongoDB with $position as a modifier to $push :

    Article.update(
        { "_id": articleId },
        { 
            "$push": { "comments": { "$each": [newComment], "$position": 0 } }
        },
        function(err,result) {
            // other work in here
        }
    );
    

    This "prepends" the array element to the existing array at the "first" (0) index so it is always at the front.

  2. Failing using "positional" updates for logical reasons or just where you "want to be sure", then there has been around for an even "longer" time the $sort modifier to $push :

    Article.update(
        { "_id": articleId },
        { 
            "$push": { 
                "comments": { 
                    "$each": [newComment], 
                    "$sort": { "$created_at": -1 } 
                }
            }
        },
        function(err,result) {
            // other work in here
        }
    );
    

    And that will "sort" on the property of the array elements documents that contains the specified value on each modification. You can even do:

    Article.update(
        {  },
        { 
            "$push": { 
                "comments": { 
                    "$each": [], 
                    "$sort": { "$created_at": -1 } 
                }
            }
        },
        { "multi": true },
        function(err,result) {
            // other work in here
        }
    );
    

    And that will sort every "comments" array in your entire collection by the specified field in one hit.

Other solutions are possible using either .aggregate() to sort the array and/or "re-casting" to mongoose objects after you have done that operation or after doing your own .sort() on the plain object.

Both of these really involve creating a separate model object and "schema" with the embedded items including the "referenced" information. So you could work upon those lines, but it seems to be unnecessary overhead when you could just sort the data to you "most needed" means in the first place.

The alternate is to make sure that fields like "virtuals" always "serialize" into an object format with .toObject() on call and just live with the fact that all the methods are gone now and work with the properties as presented.

The last is a "sane" approach, but if what you typically use is "created_at" order, then it makes much more sense to "store" your data that way with every operation so when you "retrieve" it, it stays in the order that you are going to use.

Blakes Seven
  • 49,422
  • 14
  • 129
  • 135
  • Thanks. Your methods are by far the simplest. Things get a bit awkward when you are trying to work within the Mongoose API, i.e. findById -> push -> .save().. – backdesk Dec 16 '15 at 12:10
  • 1
    @backdesk Well this **is** Mongoose API also. The difference being that updates are actually atomic rather than making modifications to a retrieved document in the client side and then saving back the changes. You can of course simply sort the array before saving back as well. But I would emplore you to rather use the atomic modifications as there is no possible chance of overwriting changes as there would be with client modifications and using `save()` – Blakes Seven Dec 17 '15 at 00:09
  • Can you please explain the "$created_at"? Thank you! – Marcos Pereira Jul 21 '17 at 01:07
  • @BlakesSeven Could you tell what will be the cost of using $position? It seems to be quite expensive if existing elements positions are going to be affected. – ashwin mahajan Jan 29 '18 at 21:22
0

You could also use JavaScript's native Array sort method after you've retrieved and populated the results:

// Convert the mongoose doc into a 'vanilla' Array:
const articles = yourArticleDocs.toObject();

articles.comments.sort((a, b) => {
  const aDate = new Date(a.updated_at);
  const bDate = new Date(b.updated_at);

  if (aDate < bDate) return -1;
  if (aDate > bDate) return 1;

  return 0;
});
Steve Brush
  • 2,911
  • 1
  • 23
  • 15
0

As of the current release of MongoDB you must sort the array after database retrieval. But this is easy to do in one line using _.sortBy() from Lodash.

https://lodash.com/docs/4.17.15#sortBy

comments = _.sortBy(sorted.comments, 'created_at').reverse();
steampowered
  • 11,809
  • 12
  • 78
  • 98