Search code examples
node.jsmongodbmongoosemongoose-populate

mongoose#populate returns null in nested object inside array


I have a mongoDB database which is generated using a script that uses only the node.js mongoDB driver without mongoose. Later on, in the application, I want to use mongoose to load a document and have a reference be populated automatically; however, this only ever returns null.

Imagine a task which contains sub-items which each have a title and an assigned person. The assigned person, in this case, is the reference I want to have populated, so the reference lives in an object inside an array in the task schema.

The following code (requiring npm install mongodb mongoose) reproduces the problem (watch out, it destroys a local database named test if you have one already):

const mongodb = require('mongodb');
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

(async () => {
    // Step 1: Insert data. This is done using the mongodb driver without mongoose.
    const db = await mongodb.MongoClient.connect('mongodb://localhost/test');
    await db.dropDatabase();
    await db.collection('persons').insertOne({ name: 'Joe' });

    const joe = await db.collection('persons').findOne({ name: 'Joe' });
    await db.collection('tasks').insertOne({ items: [{ title: 'Test', person: joe._id }] });

    await db.close();

    // ================
    // Step 2: Create the schemas and models.
    const PersonSchema = new Schema({
        name: String,
    });
    const Person = mongoose.model('Person', PersonSchema);

    const TaskSchema = new Schema({
        items: [{
            title: String,
            person: { type: Schema.Types.ObjectId, ref: 'Person' },
        }],
    });
    const Task = mongoose.model('Task', TaskSchema);

    // ================
    // Step 3: Try to query the task and have it populated.
    mongoose.connect('mongodb://localhost/test');
    mongoose.Promise = Promise;

    const myTask = await Task.findOne({}).populate('items.person');

    // :-( Unfortunately this prints only
    // { _id: "594283a5957e327d4896d135", items: [ { title: 'Test', person: null } ] }
    console.log(JSON.stringify(myTask, null, 4));

    mongoose.connection.close();
})();

The expected output would be

{ _id: "594283a5957e327d4896d135", items: [ { title: 'Test', person: { _id: "594283a5957e327d4896d134", name: "Joe" } } ] }

I have verified that the two _ids actually match using mongo shell:

> db.persons.find({})
{ "_id" : ObjectId("594283a5957e327d4896d134"), "name" : "Joe" }
> db.tasks.find({})
{ "_id" : ObjectId("594283a5957e327d4896d135"), "items" : [ { "title" : "Test", "person" : ObjectId("594283a5957e327d4896d134") } ] }

What am I doing wrong when attempting to populate person? I am using mongoose 4.10.6 and mongodb 2.2.28.


Solution

  • The answer to this problem lies in the fact that the collection name mongoose automatically infers from the model Person is people and not persons.

    The problem can be solved either by writing to the people collection in the first part or by forcing mongoose to use the collection name persons:

    const Person = mongoose.model('Person', PersonSchema, 'persons');
    

    mongoose plans to remove pluralization in the collection name anyway, see #1350 on Github.