Popolare array nidificati in mongoose


111

Come posso popolare "componenti" nel documento di esempio:

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

Questo è il mio JS da cui ottengo il documento di Mongoose:

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

È vuoto adesso? Quali risultati stai ottenendo?
WiredPrairie

2
se scrivo ...populate('pages pages.page.components').exec...ottengo la stessa cosa come indicato nel documento di esempio. Niente è cambiato.
Anton Shuvalov

Risposte:


251

Mongoose 4.5 supporta questo

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

E puoi partecipare a più di un livello profondo


14
Incredibile - così tanto più pulito! Questa è ora la risposta moderna e corretta. Documentato qui .
isTravis

@NgaNguyenDuy github.com/Automattic/mongoose/wiki/4.0-Release-Notes ha detto che questa funzione esisteva già dalla 4.0. Potresti aver sbagliato query.
Trinh Hoang Nhu

1
@TrinhHoangNhu Non ho fatto la nota di rilascio 4.0, ma sono stato provato. La mia query non restituisce nulla se lo eseguo come mangusta 4.0, ma ha funzionato bene quando aggiorno alla versione 4.5.8. La mia domanda: gist.github.com/NgaNguyenDuy/998f7714fb768427abf5838fafa573d7
NgaNguyenDuy

1
@NgaNguyenDuy Ho anche bisogno di aggiornare alla 4.5.8 per far funzionare questo !!
Vinesh

4
Sono confuso come funzionerebbe perché il percorso pages.$.page.componentnon lo è pages.$.component. Come sa guardare nell'oggetto della pagina?
Dominic

111

Per me va bene:

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

Documentazione: Model.populate


9
Il "modello:" Componente "" è davvero importante da mantenere!
Totty.js

3
Ma non dovrebbe perché quando definisco l'arbitro definisco anche il modello, questo non è proprio DRY. Comunque, grazie, funziona;)
Totty.js

Stai attento con il metodo magro. Non sarai in grado di chiamare metodi personalizzati o addirittura salvare sugli oggetti restituiti.
Daniel Kmak

lean () non è necessario nel mio caso, ma il resto funziona magnificamente.
giovanni

1
È possibile popolare un altro "livello" più profondo?
timhc22

35

Come altri hanno notato, Mongoose 4supporta questo. È molto importante notare che puoi anche ricorrere più in profondità di un livello, se necessario, sebbene non sia indicato nei documenti:

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

28

Puoi popolare più documenti nidificati in questo modo.

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

1
popolare i percorsi in array ha funzionato anche per me:populate: ['components','AnotherRef']
Yasin Okumuş

Per me nella versione 5.5.7, la notazione di array menzionata da Yasin non funzionava, invece il contatto in una stringa funziona. cioèpopulate: 'components AnotherRef'
Samih A

8

È la soluzione migliore:

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

Tutte le altre risposte sono inutilmente complicate, questa dovrebbe essere la soluzione accettata.
SeedyROM

E questo risolve il caso in cui pageha altre proprietà non popolabili.
Sira Lam

4

Ho trovato questo molto utile creare un feathersjs prima del gancio per popolare una relazione profonda a 2 livelli di riferimento. I modelli di mangusta hanno semplicemente

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

poi in feathersjs prima del gancio:

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

    return Promise.resolve(hook)
  }
}

Così semplice rispetto ad altri metodi che stavo cercando di ottenere questo risultato.


A meno che non ti preoccupi di sovrascrivere una query $ populate che potrebbe essere stata passata. In tal caso dovresti usare hook.params.query. $ Populate = Object.assign (hook.params.query. $ Populate || {}, {/ * nuovo oggetto popola qui * /})
Travis S

1

Ho trovato questa domanda tramite un'altra domanda specifica per KeystoneJS ma contrassegnata come duplicata. Se qualcuno qui potrebbe cercare una risposta Keystone, è così che ho fatto la mia query di compilazione approfondita in Keystone.

Mangusta a due livelli che utilizza KeystoneJs [duplicato]

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

Puoi farlo anche usando l' $lookupaggregazione e probabilmente il modo migliore in cui popolano ora si sta estinguendo dal 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 lo supporta

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

0

Per qualcuno che ha il problema populatee vuole anche farlo:

  • chattare con testo semplice e risposte rapide (bolle)
  • 4 collezioni di database per la chat: clients, users, rooms, messasges.
  • stessa struttura del DB dei messaggi per 3 tipi di mittenti: bot, utenti e client
  • refPatho riferimento dinamico
  • populatecon pathe modelopzioni
  • utilizzare findOneAndReplace/ replaceOnecon$exists
  • creare un nuovo documento se il documento recuperato non esiste

CONTESTO

Obbiettivo

  1. Salvare un nuovo messaggio di testo semplice nel database e popolarlo con i dati dell'utente o del cliente (2 diversi modelli).
  2. Salvare un nuovo messaggio quickReplies nel database e popolarlo con i dati dell'utente o del cliente.
  3. Salvare ogni messaggio il tipo del mittente: clients, userse bot.
  4. Popolare solo i messaggi che hanno il mittente clientso userscon i relativi modelli Mongoose. _sender type client models is clients, for user is users.

Schema del messaggio :

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

SOLUZIONE

La mia richiesta API lato server

Il mio codice

Funzione di utilità (su chatUtils.jsfile) per ottenere il tipo di messaggio che si desidera salvare:

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

Il mio lato server (utilizzando Nodejs) per ottenere la richiesta di salvataggio del messaggio:

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

SUGGERIMENTI :

Per il database:

  • Ogni messaggio è un documento stesso.
  • Invece di usare refPath, usiamo l'utility getSenderModelche viene utilizzata su populate(). Ciò è dovuto al bot. La sender.typepuò essere: userscon la sua base di dati, clientscon il suo database e botsenza un database. Il refPathvero riferimento al modello richiede, in caso contrario, Mongooose genera un errore.
  • sender._idpuò essere di tipo ObjectIdper utenti e client o nullper il bot.

Per la logica della richiesta API:

  • Sostituiamo il quickReplymessaggio (il DB dei messaggi deve avere solo una risposta rapida, ma tutti i messaggi di testo semplici che desideri). Usiamo findOneAndUpdateinvece di replaceOneo findOneAndReplace.
  • Eseguiamo l'operazione di query (il findOneAndUpdate) e l' populateoperazione con il callbackdi ciascuno. Questo è importante se non si sa se l'uso async/await, then(), exec()o callback(err, document). Per maggiori informazioni guarda il Populate Doc .
  • Sostituiamo il messaggio di risposta rapida con l' overwriteopzione e senza$set operatore di query.
  • Se non troviamo la risposta rapida, ne creiamo una nuova. Devi dirlo a Mongoose con questoupsert opzione.
  • Compiliamo solo una volta, per il messaggio sostituito o per il nuovo messaggio salvato.
  • Torniamo ai callback, qualunque sia il messaggio che abbiamo salvato con findOneAndUpdatee per il populate().
  • In populate, creiamo un riferimento al modello dinamico personalizzato con l'estensione getSenderModel. Possiamo usare il riferimento dinamico Mongoose perché sender.typefor botnon ha alcun modello Mongoose. Usiamo un database di popolamento attraverso con modele pathoptins.

Ho passato molte ore a risolvere piccoli problemi qua e là e spero che questo possa aiutare qualcuno! 😃


0

Ho lottato con questo per un'intera dannata giornata. Nessuna delle soluzioni sopra ha funzionato. L'unica cosa che ha funzionato nel mio caso per un esempio come il seguente:

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

è fare quanto segue: (supponendo di compilare dopo il recupero, ma funziona anche quando si chiama populate dalla classe Model (seguita da exec))

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

// doc is now populated

In altre parole, la proprietà del percorso più esterno deve contenere il percorso completo. Nessun percorso parzialmente completo accoppiato con le proprietà di popolamento sembrava funzionare (e la proprietà del modello non sembra essere necessaria; ha senso poiché è inclusa nello schema). Mi ci è voluto un dannato giorno intero per capirlo! Non sono sicuro del motivo per cui gli altri esempi non funzionano.

(Utilizzando Mongoose 5.5.32)


-3

Rimuovi riferimento ai documenti

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

Questo ha funzionato per me.

if (err) {
    return res.json(500);
}
Project.populate(options, function (err, projects) {
    res.json(projects);
});
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.