Remplir le tableau imbriqué dans mangouste

111

Comment puis-je remplir des "composants" dans le document d'exemple:

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

C'est mon JS où j'obtiens le document de Mongoose:

  Project.findById(id).populate('pages').exec(function(err, project) {
    res.json(project);
  });
Anton Shuvalov
la source
Est-ce vide maintenant? Quels résultats obtenez-vous?
WiredPrairie
2
si j'écris, ...populate('pages pages.page.components').exec...j'obtiens la même chose que celle indiquée dans le document d'exemple. Rien n'est changé.
Anton Shuvalov

Réponses:

251

Mongoose 4.5 prend en charge cela

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

Et vous pouvez rejoindre plus d'un niveau profond

Trinh Hoang Nhu
la source
14
Incroyable - tellement plus propre! C'est maintenant la réponse moderne et correcte. Documenté ici .
isTravis le
@NgaNguyenDuy github.com/Automattic/mongoose/wiki/4.0-Release-Notes a déclaré que cette fonctionnalité existe déjà depuis la version 4.0. Vous avez peut-être une mauvaise requête.
Trinh Hoang Nhu
1
@TrinhHoangNhu Je n'ai pas publié la note de publication 4.0, mais j'ai été essayé. Ma requête ne renvoie rien si je l'exécute en tant que mangouste 4.0, mais cela a bien fonctionné lorsque je suis passé à la version 4.5.8. Ma requête: gist.github.com/NgaNguyenDuy/998f7714fb768427abf5838fafa573d7
NgaNguyenDuy
1
@NgaNguyenDuy J'avais également besoin de mettre à jour la version 4.5.8 pour que cela fonctionne !!
vinesh
4
Je ne sais pas comment cela fonctionnerait, car le chemin pages.$.page.componentne l' est pas pages.$.component. Comment sait-il apparaître dans l'objet de page?
Dominic
111

Ça marche pour moi:

 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

Anton Shuvalov
la source
9
Le "modèle: 'Composant'" est vraiment important à conserver!
Totty.js
3
Mais ne devrait pas parce que lorsque je définis la référence, je définis également le modèle, ce n'est pas vraiment DRY. Quoi qu'il en soit, merci, ça marche;)
Totty.js
Soyez prudent avec la méthode maigre. Vous ne pourrez pas appeler de méthodes personnalisées ni même enregistrer sur les objets renvoyés.
Daniel Kmak
lean () n'est pas nécessaire dans mon cas mais le reste fonctionne à merveille.
john
1
Est-il possible de peupler un autre «niveau» plus profond?
timhc22
35

Comme d'autres l'ont noté, Mongoose 4soutient cela. Il est très important de noter que vous pouvez également revenir à plus d'un niveau, si nécessaire, bien que cela ne soit pas indiqué dans la documentation:

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

Vous pouvez remplir plusieurs documents imbriqués comme celui-ci.

   Project.find(query)
    .populate({ 
      path: 'pages',
      populate: [{
       path: 'components',
       model: 'Component'
      },{
        path: 'AnotherRef',
        model: 'AnotherRef',
        select: 'firstname lastname'
      }] 
   })
   .exec(function(err, docs) {});
Shaul Hameed
la source
1
peupler les chemins dans le tableau a également fonctionné pour moi:populate: ['components','AnotherRef']
Yasin Okumuş
Pour moi, dans la version 5.5.7, la notation de tableau mentionnée par Yasin ne fonctionnait pas, le contact dans une chaîne fonctionne à la place. iepopulate: 'components AnotherRef'
Samih A
8

C'est la meilleure solution:

Car
 .find()
 .populate({
   path: 'pages.page.components'
})
Tuấn Anh Đào
la source
Toutes les autres réponses sont inutilement compliquées, cela devrait être la solution acceptée.
SeedyROM
Et cela résout le cas où pagea d'autres propriétés non remplissables.
Sira Lam
4

J'ai trouvé cela très utile en créant un feathersjs avant le crochet pour peupler une relation profonde de niveau 2 ref. Les modèles mangouste ont simplement

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

puis dans feathersjs avant crochet:

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

    return Promise.resolve(hook)
  }
}

Si simple par rapport à d'autres méthodes, j'essayais d'y parvenir.

Travis S
la source
Sauf si vous vous inquiétez de l'écrasement d'une requête $ populate qui aurait pu être passée. Dans ce cas, vous devez utiliser hook.params.query. $ Populate = Object.assign (hook.params.query. $ Populate || {}, {/ * nouvel objet populate here * /})
Travis S
1

J'ai trouvé cette question à travers une autre question qui était spécifique à KeystoneJS mais qui a été marquée comme dupliquée. Si quelqu'un ici cherche une réponse Keystone, voici comment j'ai fait ma requête de remplissage profond dans Keystone.

Population à deux niveaux de Mongoose utilisant 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,
            });
        });
};
Léopold Kristjansson
la source
1

Vous pouvez le faire en utilisant $lookup agrégation et probablement la meilleure façon de peupler est de disparaître du 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"
  }}
])
Ashh
la source
1

Mongoose 5.4 prend en charge cela

Project.find(query)
.populate({
  path: 'pages.page.components',
  model: 'Component'
})
Nadun Liyanage
la source
0

Pour quelqu'un qui a un problème avec populateet qui souhaite également le faire:

  • discuter avec un texte simple et des réponses rapides (bulles)
  • 4 collections de base de données pour le chat: clients, users, rooms,messasges .
  • même structure de base de données de messages pour 3 types d'expéditeurs: bot, utilisateurs et clients
  • refPathou référence dynamique
  • populateavec pathetmodel options
  • utiliser findOneAndReplace/ replaceOneavec$exists
  • créer un nouveau document si le document récupéré n'existe pas

LE CONTEXTE

Objectif

  1. Enregistrez un nouveau message texte simple dans la base de données et remplissez-le avec les données de l'utilisateur ou du client (2 modèles différents).
  2. Enregistrez un nouveau message quickReplies dans la base de données et remplissez-le avec les données de l'utilisateur ou du client.
  3. Enregistrer chaque message son type d'expéditeur: clients, users&bot .
  4. Remplissez uniquement les messages qui ont l'expéditeur clientsou usersavec ses modèles Mongoose. Les modèles de client de type _sender sont clients, pour l'utilisateur users.

Schéma de message :

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

Ma demande d'API côté serveur

Mon code

Fonction utilitaire (sur chatUtils.jsfichier) pour obtenir le type de message que vous souhaitez enregistrer:

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

Mon côté serveur (en utilisant Nodejs) pour obtenir la demande d'enregistrement du message:

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

CONSEILS :

Pour la base de données:

  • Chaque message est un document lui-même.
  • Au lieu d'utiliser refPath, nous utilisons l'utilitaire getSenderModelutilisé sur populate(). C'est à cause du bot. Le sender.typepeut être: usersavec sa base de données, clientsavec sa base de données et botsans base de données. Les refPathbesoins vraie référence de modèle, sinon, Mongooose jette une erreur.
  • sender._idpeut être de type ObjectIdpour les utilisateurs et les clients, ou nullpour le bot.

Pour la logique de demande d'API:

  • Nous remplaçons le quickReplymessage (Message DB doit avoir une seule réponse rapide, mais autant de messages texte simples que vous le souhaitez). Nous utilisons le findOneAndUpdateau lieu de replaceOneou findOneAndReplace.
  • Nous exécutons l'opération de requête (le findOneAndUpdate) et l' populateopération avec le callbackde chacun. Ceci est important si vous ne savez pas si l' utilisation async/await, then(), exec()ou callback(err, document). Pour plus d'informations, consultez le document Populate Doc .
  • Nous remplaçons le message de réponse rapide par l' overwriteoption et sans $setopérateur de requête.
  • Si nous ne trouvons pas la réponse rapide, nous en créons une nouvelle. Vous devez le dire à Mongoose avec upsertoption.
  • Nous ne remplissons qu'une seule fois, pour le message remplacé ou le nouveau message enregistré.
  • Nous revenons aux rappels, quel que soit le message que nous avons enregistré avec findOneAndUpdateet pour le populate().
  • Dans populate, nous créons une référence de modèle dynamique personnalisée avec le getSenderModel. Nous pouvons utiliser la référence dynamique Mongoose car le sender.typefor botn'a pas de modèle Mongoose. Nous utilisons une base de données peuplée avec modelet pathoptins.

J'ai passé beaucoup d'heures à résoudre de petits problèmes ici et là et j'espère que cela aidera quelqu'un! 😃

Guillem
la source
0

J'ai lutté avec ça pendant une journée sanglante. Aucune des solutions ci-dessus n'a fonctionné. La seule chose qui a fonctionné dans mon cas pour un exemple comme celui-ci:

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

est de faire ce qui suit: (En supposant que le remplissage après fetch - mais fonctionne également lors de l'appel de populate à partir de la classe Model (suivi de exec))

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

// doc is now populated

En d'autres termes, la propriété de chemin la plus externe doit contenir le chemin complet. Aucun chemin partiellement complet couplé à des propriétés de peuplement ne semblait fonctionner (et la propriété de modèle ne semble pas nécessaire; elle a du sens puisqu'elle est incluse dans le schéma). Il m'a fallu une putain de journée pour comprendre ça! Je ne sais pas pourquoi les autres exemples ne fonctionnent pas.

(Utilisation de Mongoose 5.5.32)

Samuel G
la source
-3

Supprimer la référence aux documents

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

Cela a fonctionné pour moi.

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