Mongoose: Can't make nested populate in new mongoose 3.6rc0

Created on 10 Mar 2013  Â·  18Comments  Â·  Source: Automattic/mongoose

I have the following schemas:

var DocumentSchema = new Schema({
  title    : {type: String},
  emails   : [{type: ObjectId, ref: 'Emails'}]
});

var EmailSchema = new Schema({
  title    : {type: String},
  tags     : [{type: ObjectId, ref: 'Tags'}]
});

var TagSchema = new Schema({
  title    : {type: String}    
});

I want to find a document with populated 'emails' and tags. When I try this:

Document.findById(ObjectId('...'))
  populate('emails.tags')
  .exec(function (err, document) {

  });

I get the error:

Caught exception: TypeError: Cannot call method 'path' of undefined

Most helpful comment

@yangsu you are correct. Though there is another way. Either specify the model that should be used for population in the options to User.populate or use the Phone model directly:

BlogPost.find({ tags: 'fun' }).lean().populate('author').exec(function (err, docs) {
  assert.ifError(err);

  User.populate(docs, {
    path: 'author.phone',
    select: 'name',
    model: Phone // <== We are populating phones so we need to use the correct model, not User
  }, callback);

  // or use the model directly

  Phone.populate(docs, {
    path: 'author.phone',
    select: 'name',
  }, callback);

This seems clunky. We should be able to do better with this in a future release. If the documents being populated are mongoose documents (opposed to plain objects) we could possibly use it's related schema to determine which Model to use. Won't work for plain objects of course.

All 18 comments

Works as designed. See the Model.populate() section in the pre-release notes

Can you clarify how this is works as designed? I can't find anything in the docs referenced that shows me how I should approach this. I'm getting the same error.

Can't make nested populate in new mongoose v3.6.
I don't understand that "author.phone" is null.

Do I mistake it?

output

===========
    mongoose version: 3.6.0
========


dbname: testing_populateAdInfinitum
{ title: 'blog 0',
  author:
   { _id: 51559f6ad1bc1d8c1f000003,
     name: 'mary',
     phone: null,
     __v: 0 },
  _id: 51559f6ad1bc1d8c1f000007,
  tags: [ 'fun', 'cool' ],
  __v: 0 }

package.json

{
  "name": "mongoose-populate-test",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "mongoose": "3.6.0",
    "async": "0.1.22"
  },
  "engines": {
    "node": ">=0.8.21",
    "npm": ">=1.2.11"
  },
  "scripts": {
    "start": "node test.js"
  }
}

test.js

var assert = require('assert');
var mongoose = require('mongoose');
var async = require('async');
var Schema = mongoose.Schema;
var ObjectId = mongoose.Types.ObjectId;

console.log('\n===========');
console.log('    mongoose version: %s', mongoose.version);
console.log('========\n\n');

var dbname = 'testing_populateAdInfinitum';
console.log('dbname: %s', dbname);
mongoose.connect('localhost', dbname);
mongoose.connection.on('error', function () {
  console.error('connection error', arguments);
});

var phone = new Schema({
    name: String
});
var Phone = mongoose.model('Phone', phone);

var user = new Schema({
    name: String
  , phone: { type: Schema.ObjectId, ref: 'Phone' }
});
var User = mongoose.model('User', user);

var blogpost = Schema({
    title: String
  , tags: [String]
  , author: { type: Schema.ObjectId, ref: 'User' }
});
var BlogPost = mongoose.model('BlogPost', blogpost);

var createPhones = function(callback) {
  var phoneIds = [new ObjectId, new ObjectId];
  var phones = [];

  phones.push({
    _id: phoneIds[0]
    , name: 'iPhone 5'
  });
  phones.push({
    _id: phoneIds[1]
    , name: 'GALAXY S'
  });

  Phone.create(phones, function(err, docs) {
    assert.ifError(err);
    callback(null, phoneIds);
  });
};

var createUsers = function(phoneIds, callback) {
  var userIds = [new ObjectId, new ObjectId];
  var users = [];

  users.push({
    _id: userIds[0]
    , name: 'mary'
    , phone: phoneIds[0]
  });
  users.push({
    _id: userIds[1]
    , name: 'bob'
    , phone: phoneIds[1]
  });

  User.create(users, function(err, docs) {
    assert.ifError(err);
    callback(null, userIds);
  });
};

var createBlogPosts = function(userIds, callback) {
  var blogposts = [];

  blogposts.push({ title: 'blog 0', tags: ['fun', 'cool'], author: userIds[0] });
  blogposts.push({ title: 'blog 1', tags: ['cool'], author: userIds[1] });

  BlogPost.create(blogposts, function(err, docs) {
    assert.ifError(err);
    callback(null);
  });
};

var findFunBlogPosts = function(callback) {
  BlogPost.find({ tags: 'fun' }).lean().populate('author').exec(function (err, docs) {
    assert.ifError(err);

    var opts = {
        path: 'author.phone'
      , select: 'name'
    };

    BlogPost.populate(docs, opts, function(err, docs) {
      assert.ifError(err);
      docs.forEach(function(doc) {
        console.log(doc);
      });
      callback(null);
    });
  });
};

mongoose.connection.on('open', function () {
  async.waterfall([
      createPhones, 
      createUsers, 
      createBlogPosts,  
      findFunBlogPosts
    ], 
    function(err, result) {
      if (err) {
        console.error(err.stack);
      }
      mongoose.connection.db.dropDatabase(function () {
        mongoose.connection.close();
      });
    });
});

There is a context aware part of your code:
var phoneIds = [… should be 1) var outside of the func and 2) in this function only push to this array.

I ran into the same problem. I think it has to do with the fact that even when the first populate call has completed, the BlogPost model still doesn't understand the query 'authors.phone' because it wasn't defined in the BlogPost Schema. Think this is the designed behavior.

For now, this is how I resolved it. I've made changes to your code as @kuba-kubula pointed out. And I extracted the authors out and then used a populate call on User to populate phone.

Here's the code and the revisions view to see the changes I made.

Hope this helps.

@yangsu you are correct. Though there is another way. Either specify the model that should be used for population in the options to User.populate or use the Phone model directly:

BlogPost.find({ tags: 'fun' }).lean().populate('author').exec(function (err, docs) {
  assert.ifError(err);

  User.populate(docs, {
    path: 'author.phone',
    select: 'name',
    model: Phone // <== We are populating phones so we need to use the correct model, not User
  }, callback);

  // or use the model directly

  Phone.populate(docs, {
    path: 'author.phone',
    select: 'name',
  }, callback);

This seems clunky. We should be able to do better with this in a future release. If the documents being populated are mongoose documents (opposed to plain objects) we could possibly use it's related schema to determine which Model to use. Won't work for plain objects of course.

@kuba-kubula @yangsu @aheckmann
Thank you for replying, I resolved it.

@vovan22 Did you find any solution? I have the same problem. If I want to populate subcategories.products and I got the same error. https://github.com/LearnBoost/mongoose/issues/1568

BlogPost.find({ tags: 'fun' }).lean().populate('author').exec(function (err, docs) {
assert.ifError(err);

User.populate(docs, {
path: 'author.phone',
select: 'name',
model: Phone // <== We are populating phones so we need to use the correct model, not User
}, callback);

// or use the model directly

Phone.populate(docs, {
path: 'author.phone',
select: 'name',
}, callback);

The reverse populate off of the model itself is pretty nice. Thanks for this.

exports.padByID = function(req, res, next, id) {

 Pad.findById(id).populate('comments_pub').exec(function(err, pad) {
    if (err) return next(err);
    User.populate(pad.comments_pub,
                    { path : 'comments_pub.user',
                      select: 'email',
                      model: 'User'});

    if (! pad) return next(new Error('Failed to load Pad ' + id));
    req.pad = pad ;
    next();
});

};

This is simply not working for me :(. I cannot get populate() to work for me correctly, any help would be appreciated.

The comments_pub Pad property is an array of generic objects, each of which has a User model associated to it.

@aheckmann I've got the callback function returning the updated model object, which is great. However, in this context:

var User = mongoose.model('User');
var Grade = mongoose.model('Grade');

User.findOne({ ... })
  .populate('array')
  .exec(function (err, user) {
    Grade.populate(user, {
      path: 'array.grades',
      select: 'name -_id'
    }, function (error, updatedUser) {
      //assuming no error,
      user = updatedUser; //how do I persist / pass this on synchronously?
    });
  });

how do I get the user object returned from my findOne query to get the nested property data? I'm using Mongoose 4.0.x - and I've looked on S/O and through the docs of course, but if I've missed some built in way of doing it would be nice to know.

@JaKXz what do you mean by persisting? You should be able to push/pull from the user.grades array.

As the metadata for population is present in the ref property, shouldnt the libary be able to automatically guess the correct model for population?

I don't understand your question, please clarify

I think he's asking if the field you are trying to use to populate is correctly defined the Model schema.

Was this page helpful?
0 / 5 - 0 ratings