ChatGPT解决这个技术问题 Extra ChatGPT

Populate nested array in mongoose

How can I populate "components" in the example document:

  {
    "__v": 1,
    "_id": "5252875356f64d6d28000001",
    "pages": [
      {
        "__v": 1,
        "_id": "5252875a56f64d6d28000002",
        "page": {
          "components": [
            "525287a01877a68528000001"
          ]
        }
      }
    ],
    "author": "Book Author",
    "title": "Book Title"
  }

This is my JS where I get document by Mongoose:

  Project.findById(id).populate('pages').exec(function(err, project) {
    res.json(project);
  });
Is it empty now? What results are you getting?
if I write ...populate('pages pages.page.components').exec... I get the same thing as stated in example document. Nothing is changed.
how to filter documents inside pages? for example I want pages with "__V": 1 only
@MahmoodHussain please ask that as a new question

J
James

Mongoose 4.5 support this

Project.find(query)
  .populate({ 
     path: 'pages',
     populate: {
       path: 'components',
       model: 'Component'
     } 
  })
  .exec(function(err, docs) {});

And you can join more than one deep level.

Edit 03/17/2021: This is the library's implementation, what it do behind the scene is make another query to fetch thing for you and then join in memory. Although this work but we really should not rely on. It will make your db design look like SQL tables. This is costly operation and does not scale well. Please try to design your document so that it reduce join.


Amazing - so much cleaner! This is now the modern and correct answer. Documented here.
@NgaNguyenDuy github.com/Automattic/mongoose/wiki/4.0-Release-Notes said that this feature already there since 4.0. You may got wrong query.
@TrinhHoangNhu I didn't 4.0 Release Note, but i was tried. My query don't return anything if i run it as mongoose 4.0, but it worked fine when i upgrade to 4.5.8 version. My query: gist.github.com/NgaNguyenDuy/998f7714fb768427abf5838fafa573d7
@NgaNguyenDuy I also needed to update to 4.5.8 to make this work !!
I'm confused how this would work as the path is pages.$.page.component not pages.$.component. How does it know to look in the page object?
P
Pier-Luc Gendreau

That works for me:

 Project.find(query)
  .lean()
  .populate({ path: 'pages' })
  .exec(function(err, docs) {

    var options = {
      path: 'pages.components',
      model: 'Component'
    };

    if (err) return res.json(500);
    Project.populate(docs, options, function (err, projects) {
      res.json(projects);
    });
  });

Documentation: Model.populate


The "model: 'Component'" is really important to keep!
But shouldn't because when I define the ref I also define the model, this is not really DRY. Anyway, thanks, it works ;)
Be careful with lean method. You won't be able to call custom methods or even save on returned objects.
lean() isn't necessary in my case but the rest works beautifully.
Is it possible to populate another 'level' deeper?
s
suufi

As others have noted, Mongoose 4 supports this. It is very important to note that you can recurse deeper than one level too, if needed—though it is not noted in the docs:

Project.findOne({name: req.query.name})
    .populate({
        path: 'threads',
        populate: {
            path: 'messages', 
            model: 'Message',
            populate: {
                path: 'user',
                model: 'User'
            }
        }
    })

S
Shaul Hameed

You can populate multiple nested documents like this.

   Project.find(query)
    .populate({ 
      path: 'pages',
      populate: [{
       path: 'components',
       model: 'Component'
      },{
        path: 'AnotherRef',
        model: 'AnotherRef',
        select: 'firstname lastname'
      }] 
   })
   .exec(function(err, docs) {});

populate paths in array also worked for me: populate: ['components','AnotherRef']
For me in version 5.5.7, the array notation Yasin mentioned didn't work, contacting in one string instead works. i.e. populate: 'components AnotherRef'
T
Tuấn Anh Đào

It's is the best solution:

Car
 .find()
 .populate({
   path: 'pages.page.components'
})

All of the other answers are needlessly complicated, this should be the accepted solution.
And this solves the case where page has other non-populate-able properties.
M
MartinsOnuoha

If you would like to populate another level deeper, here's what you need to do:

Airlines.findById(id)
      .populate({
        path: 'flights',
        populate:[
          {
            path: 'planeType',
            model: 'Plane'
          },
          {
          path: 'destination',
          model: 'Location',
          populate: { // deeper
            path: 'state',
            model: 'State',
            populate: { // even deeper
              path: 'region',
              model: 'Region'
            }
          }
        }]
      })

was looking for multiple fields on the same level. array method worked. thanks
T
Travis S

I found this very helpful creating a feathersjs before hook to populate a 2 ref level deep relation. The mongoose models simply have

tables = new Schema({
  ..
  tableTypesB: { type: Schema.Types.ObjectId, ref: 'tableTypesB' },
  ..
}
tableTypesB = new Schema({
  ..
  tableType: { type: Schema.Types.ObjectId, ref: 'tableTypes' },
  ..
}

then in feathersjs before hook:

module.exports = function(options = {}) {
  return function populateTables(hook) {
    hook.params.query.$populate = {
      path: 'tableTypesB',
      populate: { path: 'tableType' }
    }

    return Promise.resolve(hook)
  }
}

So simple compared to some other methods I was trying to achieve this.


Unless worried about overwriting a $populate query that may have been passed in. In that case you should use hook.params.query.$populate = Object.assign(hook.params.query.$populate || {}, { /* new populate object here */})
N
Nadun Liyanage

Mongoose 5.4 supports this

Project.find(query)
.populate({
  path: 'pages.page.components',
  model: 'Component'
})

M
MD SHAYON

This is how you can make nested population

Car
  .find()
  .populate({
    path: 'partIds',
    model: 'Part',
    populate: {
      path: 'otherIds',
      model: 'Other'
    }
  })

A
Ashh

You can do this using $lookup aggregation as well and probably the best way as now populate is becoming extinct from the mongo

Project.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id) } },
  { "$lookup": {
    "from": Pages.collection.name,
    "let": { "pages": "$pages" },
    "pipeline": [
      { "$match": { "$expr": { "$in": [ "$_id", "$$pages" ] } } },
      { "$lookup": {
        "from": Component.collection.name,
        "let": { "components": "$components" },
        "pipeline": [
          { "$match": { "$expr": { "$in": [ "$_id", "$$components" ] } } },
        ],
        "as": "components"
      }},
    ],
    "as": "pages"
  }}
])

L
Leopold Kristjansson

I found this question through another question which was KeystoneJS specific but was marked as duplicate. If anyone here might be looking for a Keystone answer, this is how I did my deep populate query in Keystone.

Mongoose two level population using KeystoneJs [duplicate]

exports.getStoreWithId = function (req, res) {
    Store.model
        .find()
        .populate({
            path: 'productTags productCategories',
            populate: {
                path: 'tags',
            },
        })
        .where('updateId', req.params.id)
        .exec(function (err, item) {
            if (err) return res.apiError('database error', err);
            // possibly more than one
            res.apiResponse({
                store: item,
            });
        });
};

G
Guillem Puche

For someone who has the problem with populate and also wants to do this:

chat with simple text & quick replies (bubbles)

4 database collections for chat: clients, users, rooms, messasges.

same message DB structure for 3 types of senders: bot, users & clients

refPath or dynamic reference

populate with path and model options

use findOneAndReplace/replaceOne with $exists

create a new document if the fetched document doesn't exist

CONTEXT

Goal

Save a new simple text message to the database & populate it with the user or client data (2 different models). Save a new quickReplies message to the database and populate it with the user or client data. Save each message its sender type: clients, users & bot. Populate only the messages who have the sender clients or users with its Mongoose Models. _sender type client models is clients, for user is users.

Message schema:

const messageSchema = new Schema({
    room: {
        type: Schema.Types.ObjectId,
        ref: 'rooms',
        required: [true, `Room's id`]
    },
    sender: {
         _id: { type: Schema.Types.Mixed },
        type: {
            type: String,
            enum: ['clients', 'users', 'bot'],
            required: [true, 'Only 3 options: clients, users or bot.']
        }
    },
    timetoken: {
        type: String,
        required: [true, 'It has to be a Nanosecond-precision UTC string']
    },
    data: {
        lang: String,
        // Format samples on https://docs.chatfuel.com/api/json-api/json-api
        type: {
            text: String,
            quickReplies: [
                {
                    text: String,
                    // Blocks' ids.
                    goToBlocks: [String]
                }
            ]
        }
    }

mongoose.model('messages', messageSchema);

SOLUTION

My server side API request

My code

Utility function (on chatUtils.js file) to get the type of message that you want to save:

/**
 * We filter what type of message is.
 *
 * @param {Object} message
 * @returns {string} The type of message.
 */
const getMessageType = message => {
    const { type } = message.data;
    const text = 'text',
        quickReplies = 'quickReplies';

    if (type.hasOwnProperty(text)) return text;
    else if (type.hasOwnProperty(quickReplies)) return quickReplies;
};

/**
 * Get the Mongoose's Model of the message's sender. We use
 * the sender type to find the Model.
 *
 * @param {Object} message - The message contains the sender type.
 */
const getSenderModel = message => {
    switch (message.sender.type) {
        case 'clients':
            return 'clients';
        case 'users':
            return 'users';
        default:
            return null;
    }
};

module.exports = {
    getMessageType,
    getSenderModel
};

My server side (using Nodejs) to get the request of saving the message:

app.post('/api/rooms/:roomId/messages/new', async (req, res) => {
        const { roomId } = req.params;
        const { sender, timetoken, data } = req.body;
        const { uuid, state } = sender;
        const { type } = state;
        const { lang } = data;

        // For more info about message structure, look up Message Schema.
        let message = {
            room: new ObjectId(roomId),
            sender: {
                _id: type === 'bot' ? null : new ObjectId(uuid),
                type
            },
            timetoken,
            data: {
                lang,
                type: {}
            }
        };

        // ==========================================
        //          CONVERT THE MESSAGE
        // ==========================================
        // Convert the request to be able to save on the database.
        switch (getMessageType(req.body)) {
            case 'text':
                message.data.type.text = data.type.text;
                break;
            case 'quickReplies':
                // Save every quick reply from quickReplies[].
                message.data.type.quickReplies = _.map(
                    data.type.quickReplies,
                    quickReply => {
                        const { text, goToBlocks } = quickReply;

                        return {
                            text,
                            goToBlocks
                        };
                    }
                );
                break;
            default:
                break;
        }

        // ==========================================
        //           SAVE THE MESSAGE
        // ==========================================
        /**
         * We save the message on 2 ways:
         * - we replace the message type `quickReplies` (if it already exists on database) with the new one.
         * - else, we save the new message.
         */
        try {
            const options = {
                // If the quickRepy message is found, we replace the whole document.
                overwrite: true,
                // If the quickRepy message isn't found, we create it.
                upsert: true,
                // Update validators validate the update operation against the model's schema.
                runValidators: true,
                // Return the document already updated.
                new: true
            };

            Message.findOneAndUpdate(
                { room: roomId, 'data.type.quickReplies': { $exists: true } },
                message,
                options,
                async (err, newMessage) => {
                    if (err) {
                        throw Error(err);
                    }

                    // Populate the new message already saved on the database.
                    Message.populate(
                        newMessage,
                        {
                            path: 'sender._id',
                            model: getSenderModel(newMessage)
                        },
                        (err, populatedMessage) => {
                            if (err) {
                                throw Error(err);
                            }

                            res.send(populatedMessage);
                        }
                    );
                }
            );
        } catch (err) {
            logger.error(
                `#API Error on saving a new message on the database of roomId=${roomId}. ${err}`,
                { message: req.body }
            );

            // Bad Request
            res.status(400).send(false);
        }
    });

TIPs:

For the database:

Every message is a document itself.

Instead of using refPath, we use the util getSenderModel that is used on populate(). This is because of the bot. The sender.type can be: users with his database, clients with his database and bot without a database. The refPath needs true Model reference, if not, Mongooose throw an error.

sender._id can be type ObjectId for users and clients, or null for the bot.

For API request logic:

We replace the quickReply message (Message DB has to have only one quickReply, but as many simple text messages as you want). We use the findOneAndUpdate instead of replaceOne or findOneAndReplace.

We execute the query operation (the findOneAndUpdate) and the populate operation with the callback of each one. This is important if you don't know if use async/await, then(), exec() or callback(err, document). For more info look the Populate Doc.

We replace the quick reply message with the overwrite option and without $set query operator.

If we don't find the quick reply, we create a new one. You have to tell to Mongoose this with upsert option.

We populate only one time, for the replaced message or the new saved message.

We return to callbacks, whatever is the message we've saved with findOneAndUpdate and for the populate().

In populate, we create a custom dynamic Model reference with the getSenderModel. We can use the Mongoose dynamic reference because the sender.type for bot hasn't any Mongoose Model. We use a Populating Across Database with model and path optins.

I've spend a lot of hours solving little problems here and there and I hope this will help someone! 😃


S
Saahithyan Vigneswaran

I use following syntax that is clean. This codeblock is from my project

const result = await Result.find(filter).populate('student exam.subject')

Explanation

Say you have two schemas

Exam Schema

const ExamSchema = new mongoose.Schema({
   ...
   type: String,
   ...
})

Result Schema

const resultSchema = new mongoose.Schema({
    ...
    exam: ExamSchema,
    student: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    }
})

If I wanted to query and populate from results

Only by student id const result = await Result.find(filter).populate('student') Only by exam type const result = await Result.find(filter).populate('exam.type') By both student id and exam type const result = await Result.find(filter).populate('student exam.type')

If you need any more clarification plz ask in comments


S
Samuel G

I struggled with this for a whole bloody day. None of the solutions above worked. The only thing that worked in my case for an example like the following:

{
  outerProp1: {
    nestedProp1: [
      { prop1: x, prop2: y, prop3: ObjectId("....")},
      ...
    ],
    nestedProp2: [
      { prop1: x, prop2: y, prop3: ObjectId("....")},
      ...
    ]
  },
  ...
}

is to do the following: (Assuming populating after fetch - but also works when calling populate from the Model class (followed by exec))

await doc.populate({
  path: 'outerProp1.nestedProp1.prop3'
}).execPopulate()

// doc is now populated

In other words, the outermost path property has to contain the full path. No partially complete path coupled with populate properties seemed to work (and the model property doesn't seem to be necessary; makes sense since it is included in the schema). Took me a whole damn day to figure this out! Not sure why the other examples don't work.

(Using Mongoose 5.5.32)


R
Rafiq

Answer with one level nested populate and projection, you may find it interesting.

https://mongoplayground.net/p/2dpeZWsXR-V

query:

db.booking.aggregate([
  {
    "$match": {
      id: "61fdfeef678791001880da25"
    }
  },
  {
    $unwind: "$cart"
  },
  {
    "$lookup": {
      "from": "products",
      "localField": "cart.product",
      "foreignField": "id",
      "as": "prod"
    }
  },
  {
    "$unwind": "$prod"
  },
  {
    "$project": {
      id: 1,
      status: 1,
      cart: [
        {
          id: "$cart.id",
          date: "$cart.date",
          timeSlots: "$cart.timeSlots",
          product: {
            id: "$prod.id",
            name: "$prod.name",
            
          }
        }
      ],
      
    }
  }
])

db:

db={
  "booking": [
    {
      "status": "0",
      "cart": [
        {
          "id": "61fdffc7678791001880da5f",
          "date": "2022-02-05T00:00:00.000Z",
          "product": "61fd7bc5801207001b94d949",
          "timeSlots": [
            {
              "id": "61fd7bf2801207001b94d99c",
              "spots": 1
            }
          ],
          "createdAt": "2022-02-05T04:40:39.155Z",
          "updatedAt": "2022-02-05T04:40:39.155Z"
        }
      ],
      "version": 1,
      "id": "61fdfeef678791001880da25"
    }
  ],
  "products": [
    {
      "meta": {
        "timeZone": "America/New_York"
      },
      "photos": [],
      "name": "Guide To Toronto Canada",
      "timeSlots": [
        {
          "id": "61fd7bcf801207001b94d94d",
          "discount": null,
          "endTime": "2022-02-05T03:01:00.000Z",
          "spots": null,
          "startTime": "2022-02-04T14:00:00.000Z"
        },
        {
          "id": "61fd7bf2801207001b94d99c",
          "discount": null,
          "endTime": "2022-02-04T20:18:00.000Z",
          "spots": 15,
          "startTime": "2022-02-04T19:18:00.000Z"
        },
        
      ],
      "mrp": 20,
      "id": "61fd7bc5801207001b94d949"
    }
  ]
}

E
Eric Aya

I tried with the latest version of mongoose 5.10

Please consider the below use case of Schema definition and then review how you can populate

const jobsSchema: Schema = new Schema({
    employerId:String
}, {strict : false})

jobsSchema.virtual('employer', {
    ref: 'Employer',
    localField: 'employerId',
    foreignField: '_id',
    justOne: true 
});

Another Model is the job view

const jobsViewSchema: Schema = new Schema({
    jobId:String
}, {strict : false})

jobsViewSchema.virtual('job', {
    ref: 'Jobs',
    localField: 'jobId',
    foreignField: '_id',
    justOne: true
});

Now to populate

this.JobViewModel.find(query).populate({ 
     path: 'job', 
     populate: ['employer', 'Virtual2', 'Virtual3'] 
})

This will populate the whole object perfectly.


K
Khalid

Remove docs reference

if (err) {
    return res.json(500);
}
Project.populate(docs, options, function (err, projects) {
    res.json(projects);
});

This worked for me.

if (err) {
    return res.json(500);
}
Project.populate(options, function (err, projects) {
    res.json(projects);
});