4

I'm simply trying to call find() on a model, populate a field through a reference, then do a sort on some embedded field, which doesn't seem to be sorting at all. In other words, it seems to be a complete no-op. Below is my attempt at a minimal case (foo.js) that reproduces the problem:

const mongoose = require('mongoose');
const process = require('process');
const assert = require('assert').strict;

const Schema = mongoose.Schema;

const DogSchema = new Schema({
   name: String,
   favorite_food: { type: Schema.Types.ObjectId, ref: 'Food' }
});

const FoodSchema = new Schema({
   name: String
});

const Dog = mongoose.model('Dog', DogSchema);
const Food = mongoose.model('Food', FoodSchema);

async function setup() {
   try {
      await mongoose.connect(
         'mongodb://127.0.0.1:27017/doggy_db', { useNewUrlParser: true });
   }
   catch (err) {
      console.error(err);
      process.exit(1); 
   }

   let conn = mongoose.connection;

   try {
      await Promise.all([
         conn.dropCollection('dogs'),
         conn.dropCollection('foods')
      ]);
   }
   catch (err) {}

   let chicken = new Food({ name: 'chicken' });
   let hotdog = new Food({ name: 'hotdog' });

   await Promise.all([
      chicken.save(),
      hotdog.save()
   ]);

   await Promise.all([
      new Dog({ name: 'Snoopy', favorite_food: chicken._id }).save(),
      new Dog({ name: 'Buckie', favorite_food: hotdog._id }).save()
   ]);

   return conn;
}

async function minimal_case() {
   let conn = await setup();

   let all_dogs = await Dog.find({})
      .populate({ 
         path: 'favorite_food', 
         select: 'name',
         options: { sort: { name: 1 } } 
      });

   let favorite_food_asc = all_dogs.map(dog => dog.favorite_food.name);

   all_dogs = await Dog.find({})
      .populate({ 
         path: 'favorite_food', 
         select: 'name',
         options: { sort: { name: -1 } } 
      });

   let favorite_food_desc = all_dogs.map(dog => dog.favorite_food.name);

   assert(favorite_food_asc.join(',') === 'chicken,hotdog');
   assert(favorite_food_desc.join(',') === 'hotdog,chicken');

   conn.close();
}

minimal_case().catch(err => {
   console.error(err);
   mongoose.connection.close();
})

To run it, do,

$ mkdir db
$ mongod --dbpath db

and in some other terminal,

$ node foo.js

which should fail the second assertion in the minimal_case() function.

npm show mongoose says I'm using mongoose 5.2.5

This post also makes a mention of this:

sort in populate does not work (Mongoose)

which references this issue on github:

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

which says the issue was closed b.c. it's not supported...

and yet, the mongoose documentation, seems to mention that this kind of semantic should be possible with their API

http://mongoosejs.com/docs/api.html#query_Query-populate

my other options are to

  1. directly use native mongo syntax to leverage the aggregation pipeline (lookup, unwind, sort probably), which would make the readability of the code all choppy I guess
  2. change my model(s) so that the field I want to sort is embedded directly in the model I'm performing a query on, such that a populate isn't necessary. This could potentially be a lot of work...
  3. sort it on the client side controller as opposed to having mongo do it for me. This could potentially be really slow, probably even slower than sorting a referenced (non-indexed obviously) field
  4. anything else I'm missing?

I understand that trying to do joins and sorting on a referenced field in mongo is generally a performance hit, but I'm only doing a tutorial right now, and this incapability of trying to shoot myself in the foot caught me by surprise

UPDATE:

I added mongoose.set('debug', true) to turn on printing of queries. It seems that Dog.find({}).populate({...}) translates to two independent queries (which is even mentioned in documentation here http://mongoosejs.com/docs/populate.html):

Mongoose: dogs.find({}, { fields: {} })
Mongoose: foods.find({ _id: { '$in': [ ObjectId("5b59e1f5abaabb5b3a6bfe17"), ObjectId("5b59e1f5abaabb5b3a6bfe18") ] } }, { sort: { name: 1 }, fields: { name: 1 } })

I'm starting to wonder if it's possible to tell mongoose to chain sort semantics post-population and specify an embedded field in the query.

UPDATE:

The answer in Mongoose sort by populated field says it's not possible. I guess I'll just sort it in the controller code and try to keep a limit on what's queried.

solstice333
  • 3,399
  • 1
  • 31
  • 28

0 Answers0