Search code examples
node.jsmongoosemongoose-schemamongoose-populate

Mongoose nested model


Not sure if this would be considered a dup, but I've searched around and implemented similar queries to what I find online but can't seem to get my nested reference to work. I am simply testing to understand the mongoose syntax for populating both nested references and nested docs as discussed here: Mongoose nested schema vs nested models

However, I must be missing something because it seems to work but returns an empty array of nested references. I know that my query should return two results for the nested reference.

  • What am I overlooking or doing wrong?
  • How would I run my query differently if I wanted to use nested documents?

Data:

Result Collection:

{
    "_id" : ObjectId("5a4dcbe4ab9a793d888c9396"),
    "event_id" : ObjectId("5a482302a469a068edc004e3"),
    "event_name" : "Sample Event",
    "score" : "3-2",
    "winner" : "player1"
},

{
    "_id" : ObjectId("5a59791379cc1c321c1918f0"),
    "event_id" : ObjectId("5a482302a469a068edc004e3"),
    "event_name" : "Sample Event",
    "score" : "2-1",
    "winner" : "player2"
}

Event Collection:

{
    "_id" : ObjectId("5a482302a469a068edc004e3"),
    "type" : "Tournament",
    "name" : "Sample Event"
}

My code is as follows:

var mongoose = require("mongoose");
mongoose.connect("MongoDB://localhost/devDB");

var ResultSchema = mongoose.Schema({
    _id: mongoose.Schema.Types.ObjectId,
    event_id: {type: mongoose.Schema.Types.ObjectId, ref: "EventModel"},
    event_name: String,
    score: String,
    winner: String
});

var ResultModel = mongoose.model("results", ResultSchema);

var EventSchema = mongoose.Schema({
    _id: mongoose.Schema.Types.ObjectId,
    name: String,
    type: String,
    results: [{type: mongoose.Schema.Types.ObjectId, ref: "ResultModel"}] 
});

var EventModel = mongoose.model("events", EventSchema);


function GetEvent(eventid){
    // EventModel.findById(eventid)
    EventModel.findOne({_id: eventid})
        .populate("results","score winner", ResultModel)
        //.select("results") to extract only the nested references
        .exec(function(err, event){
            if (err){
                console.log("ERROR fetching doc: ", err.name + ":  " + err.message)
            } else{
                console.log(event);
            }
    });
}

GetEvent("5a482302a469a068edc004e3");

My output when I run it:

{ 
  results: [],
  _id: 5a482302a469a068edc004e3,
  type: 'Tournament',
  name: 'Test Tournament'
}

Solution

  • The problem with your code is that your results array is missing the ObjectIds that are necessary to populate the array. You are currently connecting the collections together with the event_id in the results documents. They refer to the documents in the event collection. However, when you are doing a populate on the events collection it expects the reference to be in the results array.


    Take these values for instance:

    Results:

    {
            "_id" : ObjectId("5a5be9a4669365067f984acb"),
            "event_name" : "Game 1",
            "score" : "1-2",
            "winner" : "ManU",
            "event_id" : ObjectId("5a5be9d9669365067f984acd")
    }
    {
            "_id" : ObjectId("5a5be9b5669365067f984acc"),
            "event_name" : "Game 2",
            "score" : "3-2",
            "winner" : "Bayern Munich",
            "event_id" : ObjectId("5a5be9d9669365067f984acd")
    }
    

    Events:

    {
            "_id" : ObjectId("5a5be9d9669365067f984acd"),
            "name" : "Champions League",
            "type" : "Cup",
            "results" : [
                    ObjectId("5a5be9a4669365067f984acb"),
                    ObjectId("5a5be9b5669365067f984acc")
            ]
    }
    

    I've manually inserted the documents with the MongoDB shell. Notice that the objectIds of the result documents are stored in the results array. If I now run this code:

    const mongoose = require("mongoose");
    mongoose.connect("mongodb://localhost/devDB");
    
    const ResultSchema = mongoose.Schema({
        event_id: {type: mongoose.Schema.Types.ObjectId, ref: "events"},
        event_name: String,
        score: String,
        winner: String
    });
    
    const Results = mongoose.model("results", ResultSchema);
    
    const EventSchema = mongoose.Schema({
        name: String,
        type: String,
        results: [{type: mongoose.Schema.Types.ObjectId, ref: "results"}] 
    });
    
    const Events = mongoose.model("events", EventSchema);
    
    function GetEvent(eventid){
        Events.findOne({_id: eventid})
            .populate("results", "score winner")
            .exec(function(err, event){
                if (err){
                    console.log("ERROR fetching doc: ", err.name + ":  " + err.message)
                } else{
                    console.log(event);
                }
        });
    }
    
    GetEvent("5a5be9d9669365067f984acd");
    

    I get the following output:

    { _id: 5a5be9d9669365067f984acd,
      name: 'Champions League',
      type: 'Cup',
      results:
       [ { _id: 5a5be9a4669365067f984acb, score: '1-2', winner: 'ManU' },
         { _id: 5a5be9b5669365067f984acc,
           score: '3-2',
           winner: 'Bayern Munich' } ] }
    

    The populated results showed up because the results array actually contained references to the result objects. The collection that the populate method looks for documents in is given in ref. The value there is the name of the registered model, i.e. the name you give as the first argument to mongoose.model().

    You are also refering to the event in the result documents with the event_id. It's not really needed with the code you have in your question, but you can leave it if you want to have a connection both ways (result <--> event). You can then create a function like this:

    function GetResult(resultid){
        Results.findOne({_id: resultid})
            .populate("event_id", "name type")
            .exec(function(err, result){
                if (err){
                    console.log("ERROR fetching doc: ", err.name + ":  " + err.message)
                } else{
                    console.log(result);
                }
        });
    }
    

    When executed like this

    GetResult("5a5be9b5669365067f984acc");
    

    it will give us this result:

    { _id: 5a5be9b5669365067f984acc,
      event_name: 'Game 2',
      score: '3-2',
      winner: 'Bayern Munich',
      event_id:
       { _id: 5a5be9d9669365067f984acd,
         name: 'Champions League',
         type: 'Cup' } }
    

    For nested models (embedding) you no longer store objectIds to other collections in a document. You store the entire document as a subdocument instead. Take for instance this code:

    const mongoose = require("mongoose");
    mongoose.connect("mongodb://localhost/devDB");
    
    const ResultSchema = mongoose.Schema({
        event_name: String,
        score: String,
        winner: String
    });
    
    // Don't register subdocuments!
    // const Results = mongoose.model("results", ResultSchema);
    
    const EventSchema = mongoose.Schema({
        name: String,
        type: String,
        results: [ResultSchema] 
    });
    
    const Events = mongoose.model("events", EventSchema);
    
    function GetEvent(eventid){
        Events.findOne({_id: eventid})
            // We no longer use populate.
            // .populate("results", "score winner")
            .exec(function(err, event){
                if (err){
                    console.log("ERROR fetching doc: ", err.name + ":  " + err.message)
                } else{
                    console.log(event);
                }
        });
    }
    
    GetEvent("5a5bf133669365067f984ace");
    

    We now store the entire result schema inside the event schema. In this particular case it's an array, but it doesn't have to be. The results collection will no longer exist. The results are store in the event documents themselves. Make sure you don't register subdocument schemas. There is also no point in having event_id so I removed that.

    I reinsert the data with the MongoDB shell:

    {
            "_id" : ObjectId("5a5bf133669365067f984ace"),
            "name" : "Champions League",
            "type" : "Cup",
            "results" : [
                    {
                            "event_name" : "Game 1",
                            "score" : "3-2",
                            "winner" : "ManU"
                    },
                    {
                            "event_name" : "Game 2",
                            "score" : "1-2",
                            "winner" : "Real Madrid"
                    }
            ]
    }
    

    And when I use GetEvents("5a5bf133669365067f984ace") I get:

    { _id: 5a5bf133669365067f984ace,
      name: 'Champions League',
      type: 'Cup',
      results:
       [ { event_name: 'Game 1', score: '3-2', winner: 'ManU' },
         { event_name: 'Game 2', score: '1-2', winner: 'Real Madrid' } ] }