Rellenar matriz anidada en mangosta


111

¿Cómo puedo completar "componentes" en el documento de ejemplo:

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

Este es mi JS donde obtengo el documento de Mongoose:

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

¿Está vacío ahora? ¿Qué resultados estás obteniendo?
WiredPrairie

2
si escribo ...populate('pages pages.page.components').exec..., obtengo lo mismo que se indica en el documento de ejemplo. Nada cambia.
Anton Shuvalov

Respuestas:


251

Mongoose 4.5 admite esto

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

Y puedes unirte a más de un nivel profundo


14
Increíble, ¡mucho más limpio! Esta es ahora la respuesta moderna y correcta. Documentado aquí .
IsTravis

@NgaNguyenDuy github.com/Automattic/mongoose/wiki/4.0-Release-Notes dijo que esta característica ya existe desde 4.0. Es posible que tenga una consulta incorrecta.
Trinh Hoang Nhu

1
@TrinhHoangNhu No tuve la nota de la versión 4.0, pero me probaron. Mi consulta no devuelve nada si la ejecuto como mongoose 4.0, pero funcionó bien cuando actualizo a la versión 4.5.8. Mi consulta: gist.github.com/NgaNguyenDuy/998f7714fb768427abf5838fafa573d7
NgaNguyenDuy

1
@NgaNguyenDuy ¡¡También necesitaba actualizar a 4.5.8 para que esto funcione !!
vinesh

4
Estoy confundido de cómo funcionaría esto ya que la ruta pages.$.page.componentno lo es pages.$.component. ¿Cómo sabe buscar en el objeto de página?
Domingo

111

Funciona para mi:

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

Documentación: Model.populate


9
¡El "modelo: 'Componente'" es muy importante de mantener!
Totty.js

3
Pero no debería porque cuando defino la referencia también defino el modelo, esto no es realmente SECO. De todos modos, gracias, funciona;)
Totty.js

Tenga cuidado con el método magro. No podrá llamar a métodos personalizados o incluso guardar en objetos devueltos.
Daniel Kmak

lean () no es necesario en mi caso, pero el resto funciona muy bien.
juan

1
¿Es posible poblar otro 'nivel' más profundo?
timhc22

35

Como han señalado otros, Mongoose 4apoya esto. Es muy importante tener en cuenta que también puede recurrir a más de un nivel, si es necesario, aunque no se indica en los documentos:

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

28

Puede completar varios documentos anidados 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
poblar rutas en la matriz también funcionó para mí:populate: ['components','AnotherRef']
Yasin Okumuş

Para mí, en la versión 5.5.7, la notación de matriz que mencionó Yasin no funcionó, en cambio, el contacto en una cadena funciona. iepopulate: 'components AnotherRef'
Samih A

8

Es la mejor solución:

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

Todas las demás respuestas son innecesariamente complicadas, esta debería ser la solución aceptada.
SeedyROM

Y esto resuelve el caso donde pagetiene otras propiedades no poblables.
Sira Lam

4

Encontré esto muy útil para crear un feathersjs before hook para poblar una relación profunda de nivel 2 ref. Los modelos de mangosta simplemente tienen

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

luego en feathersjs antes del gancho:

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

    return Promise.resolve(hook)
  }
}

Tan simple en comparación con algunos otros métodos que estaba tratando de lograr esto.


A menos que esté preocupado por sobrescribir una consulta $ populate que puede haberse pasado. En ese caso, debe usar hook.params.query. $ Populate = Object.assign (hook.params.query. $ Populate || {}, {/ * nuevo rellenar objeto aquí * /})
Travis S

1

Encontré esta pregunta a través de otra pregunta que era específica de KeystoneJS pero que estaba marcada como duplicada. Si alguien aquí podría estar buscando una respuesta de Keystone, así es como hice mi consulta de relleno profundo en Keystone.

Población de dos niveles de mangosta 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

También puede hacer esto usando $lookupagregación y probablemente la mejor manera, ya que ahora poblar se está extinguiendo del 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 admite esto

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

0

Para alguien que tiene el problema populatey también quiere hacer esto:

  • chatear con texto simple y respuestas rápidas (burbujas)
  • 4 colecciones de bases de datos para el chat: clients, users, rooms, messasges.
  • misma estructura de base de datos de mensajes para 3 tipos de remitentes: bot, usuarios y clientes
  • refPatho referencia dinámica
  • populatecon pathy modelopciones
  • usar findOneAndReplace/ replaceOnecon$exists
  • crear un nuevo documento si el documento obtenido no existe

CONTEXTO

Objetivo

  1. Guarde un nuevo mensaje de texto simple en la base de datos y complételo con los datos del usuario o del cliente (2 modelos diferentes).
  2. Guarde un nuevo mensaje de QuickReplies en la base de datos y complételo con los datos del usuario o cliente.
  3. Guardar cada mensaje de su tipo de remitente: clients, usersy bot.
  4. Complete solo los mensajes que tienen el remitente clientso userscon sus modelos Mongoose. _sender type client models is clients, for user is users.

Esquema de mensaje :

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

SOLUCIÓN

Mi solicitud de API del lado del servidor

Mi código

Función de utilidad (en chatUtils.jsarchivo) para obtener el tipo de mensaje que desea guardar:

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

El lado de mi servidor (usando Nodejs) para obtener la solicitud de guardar el mensaje:

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

CONSEJOS :

Para la base de datos:

  • Cada mensaje es un documento en sí mismo.
  • En lugar de usar refPath, usamos la utilidad getSenderModelque se usa en populate(). Esto se debe al bot. El sender.typepuede ser: userscon su base de datos, clientscon su base de datos y botsin una base de datos. El refPathverdaderas necesidades de referencia de modelo, si no, Mongooose emite un error.
  • sender._idpuede ser de tipo ObjectIdpara usuarios y clientes, o nullpara el bot.

Para la lógica de solicitud de API:

  • Reemplazamos el quickReplymensaje (Message DB debe tener solo una QuickReply, pero tantos mensajes de texto simples como desee). Usamos el en findOneAndUpdatelugar de replaceOneo findOneAndReplace.
  • Ejecutamos la operación de consulta (la findOneAndUpdate) y la populateoperación con la callbackde cada uno. Esto es importante si usted no sabe si el uso async/await, then(), exec()o callback(err, document). Para obtener más información, consulte Populate Doc .
  • Reemplazamos el mensaje de respuesta rápida con la overwriteopción y sin $setoperador de consulta.
  • Si no encontramos la respuesta rápida, creamos una nueva. Tienes que decirle a Mongoose esto con upsertopción.
  • Completamos solo una vez, para el mensaje reemplazado o el nuevo mensaje guardado.
  • Volvemos a las devoluciones de llamada, sea cual sea el mensaje que hayamos guardado con findOneAndUpdatey para populate().
  • En populate, creamos una referencia de modelo dinámica personalizada con getSenderModel. Podemos utilizar la referencia dinámica Mangosta porque el sender.typede botno tiene ningún modelo de mangosta. Usamos una base de datos que se completa con modely pathoptins.

He pasado muchas horas resolviendo pequeños problemas aquí y allá y espero que esto ayude a alguien. 😃


0

Luché con esto durante todo un maldito día. Ninguna de las soluciones anteriores funcionó. Lo único que funcionó en mi caso fue un ejemplo como el siguiente:

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

es hacer lo siguiente: (Suponiendo que se completa después de buscar, pero también funciona cuando se llama a poblar desde la clase Modelo (seguido de exec))

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

// doc is now populated

En otras palabras, la propiedad de la ruta más externa debe contener la ruta completa. No parece funcionar ninguna ruta parcialmente completa junto con propiedades de poblado (y la propiedad del modelo no parece ser necesaria; tiene sentido ya que está incluida en el esquema). ¡Me tomó un maldito día darme cuenta de esto! No estoy seguro de por qué los otros ejemplos no funcionan.

(Utilizando Mongoose 5.5.32)


-3

Quitar referencia de documentos

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

Esto funcionó para mí.

if (err) {
    return res.json(500);
}
Project.populate(options, function (err, projects) {
    res.json(projects);
});
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.