Preencher matriz aninhada em mangusto


111

Como posso preencher "componentes" no documento de exemplo:

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

Este é o meu JS, onde obtenho o documento do Mongoose:

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

Está vazio agora? Que resultados você está obtendo?
WiredPrairie

2
se eu escrever ...populate('pages pages.page.components').exec..., obtenho a mesma coisa indicada no documento de exemplo. Nada mudou.
Anton Shuvalov,

Respostas:


251

O Mongoose 4.5 suporta isso

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

E você pode ingressar em mais de um nível profundo


14
Incrível - muito mais limpo! Esta é agora a resposta moderna e correta. Documentado aqui .
is Travis

@NgaNguyenDuy github.com/Automattic/mongoose/wiki/4.0-Release-Notes disse que esse recurso já existe desde o 4.0. Você pode ter uma consulta errada.
Trinh Hoang Nhu de

1
@TrinhHoangNhu Eu não fiz o 4.0 Release Note, mas fui julgado. Minha consulta não retorna nada se eu executá-lo como mongoose 4.0, mas funcionou bem quando atualizei para a versão 4.5.8. Minha consulta: gist.github.com/NgaNguyenDuy/998f7714fb768427abf5838fafa573d7
NgaNguyenDuy

1
@NgaNguyenDuy Eu também precisei atualizar para 4.5.8 para fazer isso funcionar !!
vinesh

4
Estou confuso como isso funcionaria, pois o caminho pages.$.page.componentnão é pages.$.component. Como ele sabe olhar no objeto da página?
Dominic

111

Isso funciona para mim:

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

Documentação: Model.populate


9
O "modelo: 'Componente'" é muito importante manter!
Totty.js

3
Mas não deveria, porque quando defino o ref também defino o modelo, isso não é realmente SECO. De qualquer forma, obrigado, funciona;)
Totty.js

Tenha cuidado com o método enxuto. Você não poderá chamar métodos personalizados ou mesmo salvar em objetos retornados.
Daniel Kmak

lean () não é necessário no meu caso, mas o resto funciona perfeitamente.
john

1
É possível preencher outro 'nível' mais profundo?
timhc22

35

Como outros notaram, Mongoose 4apóia isso. É muito importante observar que você também pode percorrer mais profundamente do que um nível, se necessário, embora isso não seja mencionado nos documentos:

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

28

Você pode preencher vários documentos aninhados como este.

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

1
preencher caminhos na matriz também funcionou para mim:populate: ['components','AnotherRef']
Yasin Okumuş

Para mim, na versão 5.5.7, a notação de array que Yasin mencionou não funcionou; o contato em uma string funciona. ou sejapopulate: 'components AnotherRef'
Samih A

8

É a melhor solução:

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

Todas as outras respostas são desnecessariamente complicadas, esta deve ser a solução aceita.
SeedyROM

E isso resolve o caso em que pagetem outras propriedades não preenchíveis.
Sira Lam

4

Achei muito útil criar um penasjs antes de um gancho para preencher uma relação profunda de nível 2 de referência. Os modelos de mangusto simplesmente têm

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

então em penas, antes do anzol:

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

    return Promise.resolve(hook)
  }
}

Tão simples em comparação com alguns outros métodos que estava tentando fazer.


A menos que esteja preocupado com a substituição de uma consulta $ populate que pode ter sido passada. Nesse caso, você deve usar hook.params.query. $ Populate = Object.assign (hook.params.query. $ Populate || {}, {/ * novo objeto popular aqui * /})
Travis S

1

Eu encontrei essa pergunta por meio de outra pergunta que era específica do KeystoneJS, mas foi marcada como duplicada. Se alguém aqui está procurando uma resposta do Keystone, foi assim que fiz minha consulta de preenchimento profundo no Keystone.

População de dois níveis do Mongoose usando KeystoneJs [duplicado]

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

Você também pode fazer isso usando $lookupagregação e provavelmente a melhor forma de povoar agora está se extinguindo do 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"
  }}
])

1

Mongoose 5.4 suporta isso

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

0

Para alguém que tem problemas populatee também deseja fazer isso:

  • converse com texto simples e respostas rápidas (bolhas)
  • 4 coleções de banco de dados para bate-papo: clients, users, rooms, messasges.
  • mesma estrutura de banco de dados de mensagem para 3 tipos de remetentes: bot, usuários e clientes
  • refPathou referência dinâmica
  • populatecom pathe modelopções
  • usar findOneAndReplace/ replaceOnecom$exists
  • crie um novo documento se o documento buscado não existir

CONTEXTO

Objetivo

  1. Salve uma nova mensagem de texto simples no banco de dados e preencha-a com os dados do usuário ou cliente (2 modelos diferentes).
  2. Salve uma nova mensagem quickReplies no banco de dados e preencha-a com os dados do usuário ou cliente.
  3. Salve cada mensagem de seu tipo remetente: clients, users&bot .
  4. Preencha apenas as mensagens que têm o remetente clientsou userscom seus Modelos Mongoose. _sender type client models is clients, for user is users.

Esquema de mensagem :

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

SOLUÇÃO

Minha solicitação de API do lado do servidor

Meu código

Função utilitária (no chatUtils.jsarquivo) para obter o tipo de mensagem que deseja salvar:

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

Meu lado do servidor (usando Nodejs) para obter a solicitação de salvar a mensagem:

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

DICAS :

Para o banco de dados:

  • Cada mensagem é um documento em si.
  • Em vez de usar refPath, usamos o utilitário getSenderModelque é usado no populate(). Isso é por causa do bot. O sender.typepode ser: userscom seu banco de dados, clientscom seu banco de dados e botsem banco de dados. O refPathprecisa de uma verdadeira referência de modelo, se não, Mongooose lança um erro.
  • sender._idpode ser um tipo ObjectIdpara usuários e clientes ou nullpara o bot.

Para lógica de solicitação de API:

  • Substituímos a quickReplymensagem (Message DB deve ter apenas um quickReply, mas quantas mensagens de texto simples você quiser). Usamos o em findOneAndUpdatevez de replaceOneou findOneAndReplace.
  • Executamos a operação de consulta (o findOneAndUpdate) e a populateoperação com o callbackde cada um. Isto é importante se você não sabe se o uso async/await, then(), exec()ou callback(err, document). Para obter mais informações, consulte o Populate Doc .
  • Substituímos a mensagem de resposta rápida pela overwriteopção e sem $setoperador de consulta.
  • Se não encontrarmos uma resposta rápida, criamos uma nova. Você tem que dizer ao Mongoose isso com upsertopção.
  • Preenchemos apenas uma vez, para a mensagem substituída ou a nova mensagem salva.
  • Voltamos aos callbacks, seja qual for a mensagem que salvamos com findOneAndUpdatee para o populate().
  • Em populate, criamos uma referência de modelo dinâmico personalizado com o getSenderModel. Podemos usar a referência dinâmica Mongoose porque o sender.typefor botnão tem nenhum modelo Mongoose. Usamos um Populating Across Database com modele pathoptins.

Passei muitas horas resolvendo pequenos problemas aqui e ali e espero que isso ajude alguém! 😃


0

Lutei com isso por um dia inteiro sangrento. Nenhuma das soluções acima funcionou. A única coisa que funcionou no meu caso é um exemplo como o seguinte:

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

é fazer o seguinte: (assumindo preencher após buscar - mas também funciona ao chamar populate da classe Model (seguido por exec))

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

// doc is now populated

Em outras palavras, a propriedade do caminho mais externo deve conter o caminho completo. Nenhum caminho parcialmente completo acoplado às propriedades de preenchimento parecia funcionar (e a propriedade do modelo não parece ser necessária; faz sentido, já que está incluída no esquema). Levei um dia inteiro para descobrir isso! Não sei por que os outros exemplos não funcionam.

(Usando Mongoose 5.5.32)


-3

Remover referência de documentos

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

Isso funcionou para mim.

if (err) {
    return res.json(500);
}
Project.populate(options, function (err, projects) {
    res.json(projects);
});
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.