Add to an object to Population in a Mongoose Model

1.5k views Asked by At

I am new to Mongoose and getting my head around populations. I have Schema that looks like...

var ProjectSchema = new Schema({
    name: {
        type: String,
        default: '',
        required: 'Please fill Project name',
        trim: true
    },
    created: {
        type: Date,
        default: Date.now
    },
    topics: [{
        type: Schema.ObjectId,
        ref: 'Topic'
    }]
});

When I am querying for projects I use .populate('topics') and it works fine. When the project is populated the topics property holds actual objects rather than references.

P.S. The Topic does not have a project reference (I found it difficult to maintain resiprocal relations in MongoDB).

My question is: when I want to add a topic object to a project. Do I add the ObjectId or the object itself?

2

There are 2 answers

2
Blakes Seven On BEST ANSWER

I saw this question and an answer given but I didn't think it really explained what you asked, so I thought something was worthwile here.

As is demonstrated in the supplied schema, the "topics" property is a "referenced array" which basically means it is going to hold a "reference" ( or essentially an ObjectId value ) to the document that lies in another collection. The mongoose "schema" definition is what ties this to the "associated" model where that object resides, as you should know.

The question posed is "do I push the object, or do I just push the _id value", which in itself raises some questions about "what do you really want to do here?".

For that last, take the following code example ( presuming model and schema are all defined ):

var project = new Project({ "name": "something" });
var topic = new Topic({ "name": "something" });

project.topics.push(topic);  // this actually just adds _id since it's a ref

// More code to save topic and project in their colllections

So as per the comment in the code there, mongoose actually only adds the "_id" value into the parent document even though you asked it to "push" the entire "object". It just works out "that is what you meant to do" via the presented schema interface to the model. Not hard to do in code really, but just so you understand the underlying mechanics.

You can alternately "just use the _id value" from the object created ( after it is saved best for safety ) and add that to the array by similar means. It is much the same result:

var project = new Project({ "name": "something" });

// Yes we saved the Project object, but later...

var topic = new Topic({ "name": "something" });

topic.save(function(err,topic) {
    if (err) throw (err):            // or better handling
    project.topics.push(topic._id);  // explicit _id
});

That method is all well and fine, provided of course that by some form or other you actually have the "Objects in memory" at the time of processing for both Project and Topic with appropriate data.

On the other hand, let us presume that Project represents an object that lies in the collection, and though you know it's _id value or other "unique" representing property, that object data has not actually been loaded from the database via .findOne() type of operation or similar.

Let us then assume that you have none of the Project model data resident in memory. So how to add your new topic?

This is where the native operators for MongoDB come into play. Particularly there is $push which is of course analogous to the .push() array operator in JavaScript but with a particular "Server Side" action.

As stated earlier, you do not have the Project model data loaded, but you wish to modify a particular Project item in storage by "pushing" the desired Topic object to something defined in your Project object's collection by it's identifier:

var topic = new Topic({ "name": "something" });

topic.save(function(err,topic) {
    if (err) throw err;     // or much better handling
    Project.update(
        { "_id": projectId },
        { "$push": {
            "topics": topic._id
        }},
        function(err,numAffected) {
            // and handle responses here
        }
    );
})

The "update" mechanics can be .findOneAndUpdate() or even .findByIdAndUpdate() as you find appropriate ( those methods both return the modifed object by default where .update() does not ) to what you want to achieve as a result of your operation here.

The main difference to the previous approaches is that since the object for Project that is to be modifed does not reside in memory for your application code then you use these methods to just modify it on the server instead. This can be a "good thing" as you do not need to "load" that data just to make a modification. The MongoDB operators allow you to $push the array content without loading first.

That approach is acutally your best approach for "concurrent updates" with high transaction systems. The reason being that there is "no guarantee" that in between a .findOne() or similar operation in your application and the modifications with the eventual .save() action that "no data has been changed" on the server storage between those actions occurring.

The $push operator "ensures" that the data modified on the server remains "as it was" at the time of execution with of course the addition of the new data you added to the array.

The other obvious thing here is that since the operation is using a native MongoDB operator to achieve this effiency, the mongoose Schema rules are bybassed. So you cannot of course just "push" the entire "topic" object into the array, without of course ending up with an "entire" topic object in storage:

    Project.update(
        { "_id": projectId },
        { "$push": {
            "topics": topic   // Oops, now that would store the whole object!
        }},
        function(err,numAffected) {
            // and handle responses here
        }
    );

That is essentially the difference between, "adding an object reference" to an array and "adding an 'Object' to an array". It depends on the methods used and the "efficiency" methods you actually choose.

Hope this is useful to yourself and those others who might stumble upon the same topic you raised.

2
chridam On

You only need to save the ObjectId if Topics schema has a project ref as saving refs to other documents works the same way you normally save properties, just assign the _id value:

var topicSchema = Schema({
    _author : { type: Schema.ObjectId, ref: 'Project' },
    title    : String    
});

var Topic  = mongoose.model('Topic', topicSchema);
var Project = mongoose.model('Project', projectSchema);

var my_project = new Project({ name: 'Test Project' });

my_project.save(function (err) {
    if (err) return handleError(err);

    var topic1 = new Topic({
        title: "My Topic",
        _author: my_project._id    // assign the _id from the project
    });

    topic1.save(function (err) {
        if (err) return handleError(err);
        // thats it!
    });
});

Refer to the docs for more detail.


-- EDIT --

If the Topic schema doesn't have the project ref then you need to push the object itself:

var Topic  = mongoose.model('Topic', topicSchema);
var Project = mongoose.model('Project', projectSchema);

var my_project = new Project({ name: 'Test Project' });
var topic1 = new Topic({
    title: "My Topic"    
});

my_project.stories.push(topic1);
my_project.save(callback);