在猫鼬中填充嵌套数组


111

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

  {
    "__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);
  });

现在是空的吗?您得到什么结果?
WiredPrairie

2
如果我写的话...populate('pages pages.page.components').exec...,会得到与示例文档中所述相同的内容。一切都没有改变。
安东·舒瓦洛夫

Answers:


251

猫鼬4.5支持此

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

您可以加入不止一个深层次


14
太神奇了-如此清洁!现在,这是现代而正确的答案。 记录在这里
isTravis

@NgaNguyenDuy github.com/Automattic/mongoose/wiki/4.0-Release-Notes表示此功能自4.0开始就已经存在。您可能会收到错误的查询。
Trinh Hoang Nhu

1
@TrinhHoangNhu我没有发布4.0版本的注释,但是我已经尝试过了。如果我将其作为猫鼬4.0运行,我的查询不会返回任何内容,但是当我升级到4.5.8版本时,它运行良好。我的查询:gist.github.com/NgaNguyenDuy/998f7714fb768427abf5838fafa573d7
NgaNguyenDuy

1
@NgaNguyenDuy我也需要更新到4.5.8才能完成这项工作!
vinesh

4
我很困惑如何将工作的路径是pages.$.page.component不是pages.$.component。如何知道在页面对象中查找?
多米尼克'18

111

这对我行得通:

 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


9
保持“模型:'组件'”非常重要!
Totty.js 2014年

3
但是不应该这样,因为当我定义引用时,我也定义了模型,这并不是真正的DRY。无论如何,谢谢,它有效;)
Totty.js 2014年

使用精益方法要小心。您将无法调用自定义方法,甚至无法保存返回的对象。
Daniel Kmak 2014年

在我的情况下,lean()不是必需的,但其余的都可以很好地工作。
约翰

1
是否有可能进一步填充另一个“级别”?
timhc22 '02

35

正如其他人指出的那样,Mongoose 4对此表示支持。需要注意的是,非常重要的一点是,如果需要,您也可以递归到一个以上的级别,尽管文档中未对此进行说明:

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

28

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

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

1
在数组中填充路径也对我populate: ['components','AnotherRef']
有用

对我而言,在5.5.7版中,Yasin提到的数组符号不起作用,而是使用一个字符串进行联系。即populate: 'components AnotherRef'
Samih A


4

我发现在钩子之前创建feathersjs来填充2 ref级深度关系非常有帮助。猫鼬模型简单地具有

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

然后在hooks之前在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 || {},{/ *这里是新的填充对象* /})
Travis S'S

1

我通过另一个特定于KeystoneJS的问题找到了这个问题,但将其标记为重复。如果这里有人在寻找Keystone的答案,这就是我在Keystone中进行深度填充查询的方式。

使用KeystoneJs进行猫鼬两级人口[重复]

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

1

您也可以使用$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"
  }}
])


0

对于有问题populate并且也想这样做的人:

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

语境

目标

  1. 将新的简单文本消息保存到数据库,并用用户或客户端数据(2种不同的模型)填充它。
  2. 将新的quickReplies消息保存到数据库,并用用户或客户端数据填充它。
  3. 将每封邮件保存为发件人类型:clientsusersbot
  4. 仅填充具有发件人clients或其users猫鼬模型的邮件。_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,我们使用在getSenderModel上使用的util populate()。这是因为机器人。该sender.type可以是:users他的数据库,clients用他的数据库,bot没有数据库。在refPath需要真正的模型参考,如果没有,Mongooose抛出异常。
  • sender._id可以ObjectId为用户和客户端输入,也null可以为机器人输入。

对于API请求逻辑:

  • 我们替换quickReply消息(消息DB仅具有一个quickReply,但是您需要的数目尽可能多的简单文本消息)。我们使用findOneAndUpdate代替replaceOnefindOneAndReplace
  • 我们执行查询操作(findOneAndUpdate)和每个populate操作callback。如果您不知道使用,或async/await,这很重要。有关更多信息,请查看“ 填充文档”then()exec()callback(err, document)
  • 我们将快速回复消息替换为overwrite选项,而没有$set查询运算符。
  • 如果找不到快速答复,我们将创建一个新答复。你必须告诉猫鼬这个upsert选项。
  • 对于替换的消息或新保存的消息,我们仅填充一次。
  • 我们将返回回调,无论使用findOneAndUpdate和保存的消息是什么populate()
  • 在中populate,我们使用创建一个自定义动态模型引用getSenderModel。我们可以使用Mongoose动态参考,因为sender.typefor bot没有任何Mongoose模型。我们使用填充在多个数据库modelpathoptins。

我已经花了很多时间在这里和那里解决小问题,希望对您有所帮助!😃


0

我为此奋斗了整整一天。上述解决方案均无效。在我的案例中,唯一有效的示例如下:

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

是要执行以下操作:(假定在提取后进行填充-但在从Model类调用填充时也可以使用(其后为exec))

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

// doc is now populated

换句话说,最外层路径属性必须包含完整路径。似乎没有部分完整的路径与填充属性结合使用(并且似乎不需要模型属性;因为它包含在架构中,所以很有意义)。花了我整天的时间来解决这个问题!不知道为什么其他示例不起作用。

(使用猫鼬5.5.32)


-3

删除文档参考

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);
});
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.