node.js 在猫鼬中填充嵌套数组

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19222520/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 15:53:30  来源:igfitidea点击:

Populate nested array in mongoose

node.jsmongodbmongoose

提问by Anton Shuvalov

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:

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

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

回答by Trinh Hoang Nhu

Mongoose 4.5 support this

Mongoose 4.5 支持这个

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

And you can join more than one deep level

你可以加入多个深层次

回答by Anton Shuvalov

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

文档:Model.populate

回答by nikk wong

As others have noted, Mongoose 4supports 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:

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

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

回答by 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) {});

回答by 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

我发现在钩子之前创建一个 Feathersjs 来填充 2 ref 级别的深层关系非常有帮助。猫鼬模型只有

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:

然后在钩子之前的feathersjs中:

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.

与我试图实现这一目标的其他一些方法相比,如此简单。

回答by Tu?n Anh ?ào

It's is the best solution:

这是最好的解决方案:

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

回答by Ashh

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

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

回答by 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.

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

Mongoose two level population using KeystoneJs [duplicate]

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

回答by Guillem

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

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

  • 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
  • refPathor dynamic reference
  • populatewith pathand modeloptions
  • use findOneAndReplace/replaceOnewith $exists
  • create a new document if the fetched document doesn't exist
  • 用简单的文字和快速回复聊天(气泡)
  • 4 个用于聊天的数据库集合:clients, users, rooms, messasges.
  • 3 种类型的发件人的相同消息数据库结构:机器人、用户和客户端
  • refPath动态参考
  • populatepathmodel选项
  • 使用findOneAndReplace/replaceOne$exists
  • 如果获取的文档不存在,则创建一个新文档


CONTEXT

语境

Goal

目标

  1. Save a new simple text message to the database & populate it with the user or client data (2 different models).
  2. Save a new quickReplies message to the database and populate it with the user or client data.
  3. Save each message its sender type: clients, users& bot.
  4. Populate only the messages who have the sender clientsor userswith its Mongoose Models. _sender type client models is clients, for user is users.
  1. 将新的简单文本消息保存到数据库并使用用户或客户端数据(2 种不同的模型)填充它。
  2. 将新的 quickReplies 消息保存到数据库并用用户或客户端数据填充它。
  3. 保存每条消息的发件人类型:clients, users& bot
  4. 仅填充具有发件人clients或其usersMongoose 模型的消息。_sender 类型客户端模型是clients,对于用户是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

我的服务器端 API 请求

My code

我的代码

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

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

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

我的服务器端(使用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);
        }
    });

TIPs:

提示

For the database:

对于数据库:

  • Every message is a document itself.
  • Instead of using refPath, we use the util getSenderModelthat is used on populate(). This is because of the bot. The sender.typecan be: userswith his database, clientswith his database and botwithout a database. The refPathneeds true Model reference, if not, Mongooose throw an error.
  • sender._idcan be type ObjectIdfor users and clients, or nullfor the bot.
  • 每条消息本身就是一个文档。
  • refPath我们不使用using ,而是使用在getSenderModel上使用的 util populate()。这是机器人的原因。该sender.type可以是:users他的数据库,clients用他的数据库,bot没有数据库。在refPath需要真正的模型参考,如果没有,Mongooose抛出异常。
  • sender._id可以是ObjectId用户和客户的类型,也null可以是机器人的类型。

For API request logic:

对于 API 请求逻辑:

  • We replace the quickReplymessage (Message DB has to have only one quickReply, but as many simple text messages as you want). We use the findOneAndUpdateinstead of replaceOneor findOneAndReplace.
  • We execute the query operation (the findOneAndUpdate) and the populateoperation with the callbackof 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 overwriteoption and without $setquery operator.
  • If we don't find the quick reply, we create a new one. You have to tell to Mongoose this with upsertoption.
  • 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 findOneAndUpdateand 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.typefor bothasn't any Mongoose Model. We use a Populating Across Databasewith modeland pathoptins.
  • 我们替换quickReply消息(消息数据库必须只有一个 quickReply,但只要你想要多少简单的文本消息)。我们使用findOneAndUpdate代替replaceOnefindOneAndReplace
  • 我们执行查询操作 (the findOneAndUpdate) 和populate操作与callback每个。如果您不知道是否使用、或async/await,这一点很重要。有关更多信息,请查看Populate Docthen()exec()callback(err, document)
  • 我们用overwrite选项替换了快速回复消息,而没有$set查询运算符。
  • 如果我们没有找到快速回复,我们会创建一个新回复。你必须用upsert选项告诉猫鼬。
  • 我们只填充一次,用于替换的消息或新保存的消息。
  • 我们返回到回调,无论我们findOneAndUpdatepopulate().
  • 在 中populate,我们使用getSenderModel. 我们可以使用 Mongoose 动态引用,因为sender.typeforbot没有任何 Mongoose 模型。我们使用带有和选项的跨数据库填充。modelpath


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

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

回答by 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))

是执行以下操作:(假设在 fetch 之后填充 - 但在从 Model 类调用 populate 时也有效(后跟 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)

(使用猫鼬 5.5.32)