Today at the office I realized of an interesting use case on MongoDB and Mongoose: Many to Many relationship. I had no idea if Mongoose was going to fall in an infinite loop trying to populate each nested document but turns that it populates only the child documents (first level) 8)
On a standard SQL DataBase this kind of relationship is easy to accomplish by using a table to store the ids of the items you want to be related but if you are on a NoSQL DataBase and you want to achieve the same result… well, it is not exactly the same way, but it is not hard either. Let’s see an example.
Let’s say you have two collections: Posts and Attachments. Usually you may be able to Add many attachments to a post, or maybe you want to share a file to many posts, but since we want to do something more interesting we are going to support both: Attach multiple files to a post but also allow your attachments to be shared on multiple posts
A common way to handle documents related on a NoSQL DB is to create sub-documents this allows you to do a one-to-many relation but since we want a many-to-many then what we will do is to use the nice “Populate” method available on Mongoose and save ID references as an array, either attachments IDs on a Post, post IDs on an Attachment or even both!
You can see the code below and an example output, or if you prefer you can also download or fork it on GitHub.
// Dependencies
var mongoose = require('mongoose'),
async = require('async');
// Connect to Mongoose
mongoose.connect('mongodb://localhost:27017/circular');
var Schema = mongoose.Schema,
ObjectId = Schema.Types.ObjectId;
// Schema definitions
var PostSchema = new Schema({
title: {
type: String,
'default': ''
},
content: {
type: String,
'default': ''
}
});
var AttachmentSchema = new Schema({
fileName: {
type: String,
'default': ''
}
});
// Circular reference definitions
PostSchema.add({
attachments: [{
type: ObjectId,
ref: 'Attachment'
}]
});
AttachmentSchema.add({
posts: [{
type: ObjectId,
ref: 'Post'
}]
});
// A couple of methods to save references inside the documents by using only
// the _id instead of creating a subdocument.
PostSchema.methods.attach = function(attachment, callback) {
var post = this;
this.attachments.push(attachment);
this.save(function(err) {
attachment.posts.push(post);
attachment.save(callback);
});
};
AttachmentSchema.methods.share = function(post, callback) {
var attachment = this;
this.posts.push(post);
this.save(function(err) {
post.attachments.push(attachment);
post.save(callback);
});
};
// Models definition
var Post = mongoose.model('Post', PostSchema, 'posts');
var Attachment = mongoose.model('Attachment', AttachmentSchema, 'attachments');
// Go parallel!
async.parallel({
post: function(callback) {
console.log('creating a post');
var p = new Post({
title: 'Test post (' + Date.now() + ')',
content: 'Test post (' + Date.now() + ')'
});
p.save(callback);
},
attachment: function(callback) {
console.log('creating an attachment');
var a = new Attachment({
fileName: 'test_' + Date.now() + '.txt'
});
a.save(callback);
}
}, function(err, res) {
// Once we have a new post and a new attachment, create a relation on each one,
// this is only a demo of having the reference of each other but it is not required.
res.post[0].attach(res.attachment[0], function(err, res) {
// Dump the current data, just to see if it is working
async.series([function(callback) {
// Read all our posts and their attachments
Post.find().populate('attachments').exec(function(err, posts) {
if (err) {
return callback(err);
}
posts.forEach(function(post) {
console.log('Post ' + post.title + ' has ' + post.attachments.length + ' attachment(s):');
post.attachments.forEach(function(attachment) {
console.log('- Filename: ' + attachment.fileName);
});
});
callback();
});
}, function(callback) {
// Read all our attachments and their posts
Attachment.find().populate('posts').exec(function(err, attachments) {
if (err) {
return callback(err);
}
attachments.forEach(function(attachment) {
console.log('Attachment ' + attachment.fileName + ' is shared in ' + attachment.posts.length + ' post(s):');
attachment.posts.forEach(function(post) {
console.log('- Post: ' + post.title);
});
});
callback();
});
}], function(err) {
mongoose.disconnect();
});
});
});
After executing this file some times, I got an output like this one:
> node index.js creating a post creating an attachment Post Test post (1354674892970) has 1 attachment(s): - Filename: test_1354674892973.txt Post Test post (1354674914843) has 1 attachment(s): - Filename: test_1354674914845.txt Attachment test_1354674892973.txt is shared in 1 post(s): - Post: Test post (1354674892970) Attachment test_1354674914845.txt is shared in 1 post(s): - Post: Test post (1354674914843)