【问题标题】:Populate nested array in mongoose在猫鼬中填充嵌套数组
【发布时间】:2015-11-24 13:58:48
【问题描述】:

如何在示例文档中填充“组件”:

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

这是我的 JS,我通过 Mongoose 获取文档:

  Project.findById(id).populate('pages').exec(function(err, project) {
    res.json(project);
  });

【问题讨论】:

  • 现在是空的吗?你得到了什么结果?
  • 如果我写...populate('pages pages.page.components').exec...,我会得到与示例文档中所述相同的内容。什么都没有改变。
  • 如何过滤页面内的文档?例如,我想要带有“__V”的页面:仅 1
  • @MahmoodHussain 请提出一个新问题

标签: node.js mongodb mongoose


【解决方案1】:

用一层嵌套填充和投影来回答,你可能会觉得很有趣。

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

查询:

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={
  "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"
    }
  ]
}

【讨论】:

    【解决方案2】:

    这就是你可以制作嵌套人口的方法

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

    【讨论】:

      【解决方案3】:

      Mongoose 4.5 支持这个

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

      而且您可以加入多个深层。

      编辑 2021 年 3 月 17 日:这是库的实现,它在幕后所做的是进行另一个查询为您获取内容,然后加入内存。虽然这项工作我们真的不应该依赖。它将使您的数据库设计看起来像 SQL 表。这是昂贵的操作并且不能很好地扩展。请尝试设计您的文档以减少连接。

      【讨论】:

      • 太棒了——干净多了!现在这是现代且正确的答案。 Documented here.
      • @NgaNguyenDuy github.com/Automattic/mongoose/wiki/4.0-Release-Notes 说这个功能从 4.0 开始就已经存在了。您可能得到了错误的查询。
      • @TrinhHoangNhu 我没有 4.0 发行说明,但我试过了。如果我将它作为 mongoose 4.0 运行,我的查询不会返回任何内容,但是当我升级到 4.5.8 版本时它运行良好。我的查询:gist.github.com/NgaNguyenDuy/998f7714fb768427abf5838fafa573d7
      • @NgaNguyenDuy 我还需要更新到 4.5.8 才能完成这项工作!!
      • 我很困惑这将如何工作,因为路径是 pages.$.page.component 而不是 pages.$.component。它是如何知道查看页面对象的?
      【解决方案4】:

      我使用以下干净的语法。这个代码块来自我的项目

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

      说明

      假设你有两个架构

      考试架构

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

      结果架构

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

      如果我想从结果中查询和填充

      1. 仅限学生证

        const result = await Result.find(filter).populate('student')
        
      2. 仅按考试类型

        const result = await Result.find(filter).populate('exam.type')
        
      3. 通过学生 ID 和考试类型

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

      如果您需要更多说明,请在 cmets 中询问

      【讨论】:

        【解决方案5】:

        如果您想更深入地填充另一个级别,请执行以下操作:

        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'
                    }
                  }
                }]
              })
        

        【讨论】:

        • 正在寻找同一级别的多个字段。数组方法有效。谢谢
        【解决方案6】:

        Mongoose 5.4 支持这个

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

        【讨论】:

          【解决方案7】:

          我为此苦苦挣扎了整整一天。上述解决方案均无效。在我的情况下,唯一适用的示例如下:

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

          是执行以下操作:(假设在 fetch 之后填充 - 但在从 Model 类调用填充时也可以工作(后跟 exec))

          await doc.populate({
            path: 'outerProp1.nestedProp1.prop3'
          }).execPopulate()
          
          // doc is now populated
          

          换句话说,最外面的路径属性必须包含完整路径。没有部分完整的路径与填充属性相结合似乎有效(并且模型属性似乎不是必需的;因为它包含在架构中,所以很有意义)。我花了整整一天的时间才弄清楚这一点!不知道为什么其他示例不起作用。

          (使用猫鼬 5.5.32)

          【讨论】:

            【解决方案8】:

            这是最好的解决方案:

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

            【讨论】:

            • 所有其他答案都不必要地复杂,这应该是公认的解决方案。
            • 这解决了page 具有其他不可填充属性的情况。
            【解决方案9】:

            对于对populate 有疑问并希望这样做的人:

            • 用简单的文字和快速回复(气泡)聊天
            • 4 个用于聊天的数据库集合:clientsusersroomsmessasges
            • 相同的消息数据库结构适用于 3 种类型的发件人:机器人、用户和客户端
            • refPathdynamic reference
            • populatepathmodel 选项
            • 使用findOneAndReplace/replaceOne$exists
            • 如果获取的文档不存在,则创建一个新文档

            上下文

            目标

            1. 将新的简单文本消息保存到数据库并使用用户或客户端数据填充它(2 个不同的模型)。
            2. 将新的 quickReplies 消息保存到数据库并使用用户或客户端数据填充它。
            3. 保存每封邮件的发件人类型:clientsusers & bot
            4. 仅使用其 Mongoose 模型填充发件人为 clientsusers 的邮件。 _sender 类型客户端模型是clients,对于用户是users

            消息架构

            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);
            

            解决方案

            我的服务器端 API 请求

            我的代码

            实用函数(在chatUtils.js 文件上)获取您要保存的消息类型:

            /**
             * 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
            };
            
            

            我的服务器端(使用Nodejs)获取保存消息的请求:

            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);
                    }
                });
            

            提示

            对于数据库:

            • 每条消息本身就是一个文档。
            • 我们不使用refPath,而是使用populate() 上使用的实用程序getSenderModel。这是因为机器人。 sender.type 可以是:users 有他的数据库,clients 有他的数据库,bot 没有数据库。 refPath 需要真正的模型引用,如果没有,Mongooose 会抛出错误。
            • sender._id 可以为用户和客户端输入 ObjectId,或者为机器人输入 null

            对于 API 请求逻辑:

            • 我们替换了quickReply 消息(消息数据库必须只有一个快速回复,但您需要多少简单的文本消息都可以)。我们使用findOneAndUpdate 而不是replaceOnefindOneAndReplace
            • 我们执行查询操作(findOneAndUpdate)和populate操作,每个操作都带有callback。如果您不知道是否使用 async/awaitthen()exec()callback(err, document),这一点很重要。如需更多信息,请查看Populate Doc
            • 我们将快速回复消息替换为 overwrite 选项且不带 $set 查询运算符。
            • 如果我们没有找到快速回复,我们会创建一个新回复。您必须使用 upsert 选项告诉 Mongoose。
            • 我们只填充一次,用于替换消息或新保存的消息。
            • 我们返回回调,无论我们使用findOneAndUpdatepopulate() 保存的消息是什么。
            • populate 中,我们使用getSenderModel 创建自定义动态模型引用。我们可以使用 Mongoose 动态引用,因为 botsender.type 没有任何 Mongoose 模型。我们使用 Populating Across Databasemodelpath optins。

            我花了很多时间在这里和那里解决小问题,我希望这会对某人有所帮助! ?

            【讨论】:

              【解决方案10】:

              您也可以使用 $lookup 聚合来做到这一点,而且现在填充的最佳方式可能正在从 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"
                }}
              ])
              

              【讨论】:

                【解决方案11】:

                我通过另一个特定于 KeystoneJS 但被标记为重复的问题找到了这个问题。如果这里有人可能正在寻找 Keystone 的答案,这就是我在 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,
                            });
                        });
                };
                

                【讨论】:

                  【解决方案12】:

                  正如其他人所指出的,Mongoose 4 支持这一点。非常重要的是要注意,如果需要,您也可以进行更深层次的递归——尽管文档中没有说明:

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

                  【讨论】:

                    【解决方案13】:

                    您可以像这样填充多个嵌套文档。

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

                    【讨论】:

                    • 填充数组中的路径也对我有用:populate: ['components','AnotherRef']
                    • 对我来说,在 5.5.7 版本中,Yasin 提到的数组表示法不起作用,而是在一个字符串中联系。即populate: 'components AnotherRef'
                    【解决方案14】:

                    我发现在 hook 之前创建一个 feathersjs 来填充 2 ref 级别的深度关系非常有用。猫鼬模型只是有

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

                    然后在钩子之前的feathersjs中:

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

                    与我尝试实现的其他一些方法相比,它非常简单。

                    【讨论】:

                    • 除非担心覆盖可能已传入的 $populate 查询。在这种情况下,您应该使用 hook.params.query.$populate = Object.assign(hook.params.query.$populate || {}, { /* 在此处新建填充对象 */})
                    【解决方案15】:

                    删除文档参考

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

                    这对我有用。

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

                    【讨论】:

                      【解决方案16】:

                      这对我有用:

                       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);
                          });
                        });
                      

                      文档:Model.populate

                      【讨论】:

                      • 保留“model: 'Component'”真的很重要!
                      • 但不应该,因为当我定义 ref 时我也定义了模型,这并不是真正的 DRY。无论如何,谢谢,它有效;)
                      • 谨慎使用精益方法。您将无法调用自定义方法,甚至无法保存返回的对象。
                      • lean() 在我的情况下不是必需的,但其余的效果很好。
                      • 是否可以更深地填充另一个“级别”?
                      猜你喜欢
                      • 2021-10-15
                      • 2019-02-24
                      • 2021-12-25
                      • 1970-01-01
                      • 2015-02-09
                      • 2021-11-07
                      • 2014-07-28
                      • 2020-01-30
                      • 2019-01-16
                      相关资源
                      最近更新 更多