c1moore
c1moore

Reputation: 1867

Automatically Populate Subdocuments After Saving

I would like to always populate subdocuments after saving a particular Model automatically. What I would really like is something like below:

MyModel.post('save', function(doc, next) {
    doc.populate('path').then(next);
});

However, the above won't work because

post middleware do not directly receive flow control, e.g. no next or done callbacks are passed to it. post hooks are a way to register traditional event listeners for these methods.

Of course, there are "Asynchronous Post Hooks", but they still do not receive control flow so there is no guarantee the subdocuments will be populated when I need it.

Why not just use an embedded document? For this case, the subdocuments are the same as the parent document (e.g. I'm using something like the Composite pattern), which would cause a circular dependency that I'm not certain how to resolve (or if I can even resolve it). For other instances, I might want to be able to access the subdocuments without going through the parent document.

Another approach I considered is something like:

const nativeSave = /* ? */;

MyModel.methods.save = function save() {
    return nativeSave.call(this).then(function(savedDoc) {
        return savedDoc.populate('path');
    });
};

However, there are two problems with this. First of all, it seems a like a round-about solution. If there is a more native approach that doesn't go against mongoose's implementation, I would prefer that. Secondly, I don't know what I would set nativeSave to (as apparent by the /* ? */).

So, what are some suggestions for getting this behavior? If my second solution would be the best with the current version of mongoose, what should I set nativeSave to? I've already considered embedded documents, so please don't suggest using them unless you are providing a suggestion about resolving the circular dependency. Even then, I would like a solution for the other use-case I mentioned.

As explained in my comment, this is not the same as manually populating a subdocument after saving as other posts have asked. I want this to happen automatically to avoid leaking my implementation details (e.g. using ObjectIds instead of real documents).

Upvotes: 4

Views: 5875

Answers (4)

Renato Teixeira Lima
Renato Teixeira Lima

Reputation: 19

// extract only key of body
// const updates = Object.keys(req.body);
let model = Model.findById({_id}
// map automatically attributes
_.extend(congregation, req.body); // using lodash

// OR
// updates.forEach(update => {
//   model[update] = req.body[update];
// });

await model.save().then(model => // without curly braces
      model
        .populate('a')
        .populate('b')
        .populate('c')
        .populate('d')
        .execPopulate(),
    );

res.status(201).send(model);

Upvotes: 0

David Ghulijanyan
David Ghulijanyan

Reputation: 31

This will help

http://frontendcollisionblog.com/mongodb/2016/01/24/mongoose-populate.html

 var bandSchema = new mongoose.Schema({
  name: String,
  lead: { type: mongoose.Schema.Types.ObjectId, ref: 'person' }
});

var autoPopulateLead = function(next) {
  this.populate('lead');
  next();
};

bandSchema.
  pre('findOne', autoPopulateLead).
  pre('find', autoPopulateLead);

var Band = mongoose.model('band', bandSchema);

Upvotes: 3

c1moore
c1moore

Reputation: 1867

Ideology

I actually don't consider this "monkey patching" nor even bad practice. When you think of it, models (such as the MyModel example above) inherit from mongoose's Model class. Therefore, I consider it more extending the base save() behavior. This is one of the reasons why the maintainers refuse to throw an error if you override a mongoose method and/or use a reserved keyword.

I prefer encapsulating data where possible, leaving the implementation a secret to the outside world. In this case, I do not want any external code to ever know I am using ObjectIds in the database. Instead, I want it to think I always use the subdocument directly. If I leaked my implementation details (using ObjectIds), my code would become too tightly coupled, making maintenance and updating a nightmare. There are plenty of resources that go more in depth about encapsulation and its benefits.

Furthermore, I believe in SoC and modularizing your code. I feel making your code too dependent on Mongoose's implementation of save() (or any other method) makes your code way too fragile and Uncle Bob seems to agree. If mongoose ever dies, we switch to another DBMS, or save()'s implementation ever changes in a major way, we're screwed if I depend on the implementation details of Mongoose. I like my code being as separate from Mongoose and other libraries I use as possible.

If this were a normal, multi-threaded OO language, we probably wouldn't even be having this discussion. The save() method would just be inherited and block until the subdocuments are populated.

Disclaimer

Extending the default behavior of Mongoose's methods can be dangerous and you should do so with extreme caution. Make sure you understand what you are doing, have considered the consequences and alternatives, and have tested extensively. You may also want to discuss this approach with your team.

If you are in doubt and/or you believe Robert Moskal's solution is sufficient for your needs, I suggest using that approach. In fact, I use a similar approach in many of my models.

Considerations

Modifying save() will affect every instance of save(). If this is not desired, consider Robert Moskal's approach. In my case, I always want the subdocuments to be populated automatically after saving.

This approach will also have an affect on performance. If you have many deeply nested documents, this approach may not be appropriate for you. Perhaps Robert Moskal's solution or defining methods on your model that return Promises (or use async/await) would be more appropriate. In my case, the performance affect is negligible and acceptable.

Also, many of the concepts discussed in the Ideology section is very important and applicable to this situation. This also suggests this approach is appropriate.

Solution

As I mentioned above, I consider MyModel as inheriting from mongoose's Model class. So, I can extend the behavior of MyModel's save() method the same way I would extend the behavior of any subclass's inherited method:

MyModel.methods.save = function save(options, callback) {
    return mongoose.Model.prototype.save.call(this, options).then(function(savedDoc) {
        return savedDoc.populate('path');
    }).then(
        function(populatedDoc) {
            if(callback) {
                callback(null, populatedDoc);
            }

            return populatedDoc;
        },
        function(error) {
            if(callback) {
                return callback(error);
            }

            throw error;
        }
    });
};

Unfortunately, JS doesn't really have a concept of super, so just like any other inherited method being extended in JS I have to know the superclass to access the method. Other than that, everything is pretty simple and straight-forward.

Upvotes: 2

Robert Moskal
Robert Moskal

Reputation: 22553

I'm going to say that even if it were possible to monkeypatch a built in mongoose method like "save" or "find" it would probably be a terrible idea. Aside from the fact that not every call to the save method needs to incur the overhead of the extra populate call, you certainly wouldn't want to change the way the function works by dropping down to the underlying mongo driver (you lose validations, life cycle methods, and if you want to work with a mongoose document, you'll have to requery the database for it).

You run a huge risk breaking any code that depends on "save" working a certain way. All sorts of plugins are off the table, and you risk astonishing any developers that come after you. I wouldn't allow it in a codebase I was responsible for.

So you are left with create a static or schema method. In that method you'll call save, followed by populate. Something like this:

MyModel.methods.saveAndPopulate = function(doc) {
  return doc.save().then(doc => doc.populate('foo').execPopulate())
}

That's pretty much the most up to date approach suggested here: Mongoose populate after save. That's why I voted to close your question.

Upvotes: 6

Related Questions