Ryan Wheale
Ryan Wheale

Reputation: 28410

Making a mongoose model aware that it is nested

Consider the blog/comment schemas where nesting is appropriate (even if you disagree):

var CommentSchema = new Schema({ name: String, body: String });
var BlogPostSchema = new Schema({ title: String, comments: [CommentSchema] });

I understand how to add, update, delete comments for a blog post, but all of these methods require the save() method to be called on the parent blog post document:

blog_post.comments.push( new Comment({...}) );
blog_post.save();

I would like to be able to make the Comment schema aware that it is nested inside of another schema so that I can call save() on a comment document and it's smart enough to update the parent blog post. In my app logic, I already know the blog post id, so I would like to do something like this:

CommentSchema.virtual('blog_post_id');
CommentSchema.pre('save', function (next) {
    var comment = this;
    if( !comment.blog_post_id ) throw new Error('Need a blog post id');
    BlogModel.findById( comment.blog_post_id, function(err, post) {
        post.comments.push( comment );
        post.save(next);
    });
});

var comment = new Comment({ blog_post_id: 123, name: 'Joe', body: 'foo' });
comment.save();

The above works, but I still end up with a top-level Comments collection separate from the blog posts (this is just how mongoose works, I accept that).

Question: How do I prevent Mongoose from creating a separate "Comments" collection. In the pre-save method I would like to call next() without any write operations taking place afterwards. Any thoughts?

Upvotes: 0

Views: 612

Answers (2)

Cenk Gündoğan
Cenk Gündoğan

Reputation: 51

Using the mongoose-relationship plugin from https://www.npmjs.org/package/mongoose-relationship it is possible to make your documents aware of their relations.

Corresponding references are updated by the plugin when adding/removing documents.

There is a good example on the github page: https://github.com/sabymike/mongoose-relationship

Upvotes: 0

Ryan Wheale
Ryan Wheale

Reputation: 28410

This has earned me the Tumbleweed badge... hooray!?!?

So I have written a lot of code which accomplished the above. I don't want to release it until I have done more testing. But if anybody is interested in this, please let me know by posting here. I will gladly hand over what I have (which is going into production soon).

Right now my code doesn't support deep nesting... meaning you can only work with "simple" nesting similar to the blog/comments example above. I have the architecture in place to handle more complex nesting in the future, but I don't have the time to test right now (darn deadlines). Here are some of the big points about my solution so far:

  • All operations require the parent document's id (this makes sense once you start using it)
  • find, findOne, save, and remove directly on a nested model
  • findById doesn't (can't) work - well it maybe could work but would require searching the entire collection, which is slow. Must use findOne + parent id instead (see examples).
  • Super fast - uses projection for finding, and saves using Model.update() on the parent model (which is really fast).
  • All middleware still executes (pre/post and validation)
  • None of the findAndUpdate/Remove methods work [yet?]

Setup

// setup the "nestedSchema" plugin
var nestedSchema = require("./plugins/nestedSchema");
CommentSchema.plugin(nestedSchema, {
    path: 'comments',
    ownerModelPath: './BlogPostModel',  
    ownerIdFieldName: 'blogpost_id'
});

Examples - take note that the parent's blogpost_id is ALWAYS used - this is a requirement which makes it stay fast (callbacks and error handling removed for brevity):

// create a new comment
var comment = new CommentModel({
    blogpost_id: [id],
    name: 'Joe Schmoe',
    body: 'The content of the comment'
});
comment.save();

// use findOne in leu of findById
CommentModel.findOne({blogpost_id: [id], _id: [id]}, function( err, comment ) {
    comment.set('body', 'This comment has been updated directly!');
    comment.save();
});

// find all hateful comments and remove
CommentModel.find({blogpost_id: [id], body: /sucks|stupid|dumb/gi}).remove();

Upvotes: 2

Related Questions