1 to N bidirectional relationship in the MongoDB

After talk about data paradigm, lets take a look in the MongoDB, a NoSQL database that stores documents in json format.

It is a high-performance, non-relational database and alternative way to store data.

But here, for the non-relational nature of the technology, I intend to explore how to build a relationship between documents (note we have documents instead of tables) and navigate in both ways to recovery the data.

Yes, I know, that sounds silly, but it is because we usually work with relational database… another philosophy. But it is not the only way.

Like before, mongo is high-performance and easy way to handle with data.

To achieve that, I’ll build a very simple nodejs API. I also will use the VS Code (It is light, fast and smart text editor. It works perfectly to code and is multi-platform). So, in order to follow the steps, install all of them.

In a folder, create a file app.js. Open the directory in the VS Code (that is the way to “manage” a project) and insert the line below in app.js for test your node:

console.log("hello world!");

After that, open a command prompt, navigate to the app folder and type “node app”:

If you viewed something like that, fine so far! Now type “npm init y“. This will initialize a “package.json”, a config file to the app.

The ideia is simple: a very short and basic API to make a blog post and add comments to the post:

In your promt, type the next commands:

  • “npm install mongoose –save”: Driver to access the mongo database.
  • “npm install express –save”: Node web server framework.
  • “npm install body-parser –save”: Library to help to handle with the posted data.
Go back to the app.js, clear it and lets setup the database connection:
// mongodb setup
const mongoose = require('mongoose');
const Schema = mongoose.Schema
mongoose.connect('<YOU CONNECTION STRING HERE!!!>');

Here is an example of a mongo connection string!

Now we can setup our data schema:

// schemas of the documents
const BlogPostSchema = mongoose.Schema({
  Title: { type: String },
  Description: { type: String },

  // array defnition with the object reference from Comment
  Comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }] 
}); 

const CommentSchema = mongoose.Schema({
  // object reference from BlogPost
  BlogPost  : { type: mongoose.Schema.Types.ObjectId, ref: 'BlogPost' },
  Message: { type: String }
});

I think the unique special thing so far is the “ObjectId”. It is how we define a kind of “foreign key”, a relation between documents in the mongo. And to stablish the N comments, we have an array “[]”.

But now the things will become trick. When you receive a “comment” to save, you will need to do that in both documents. OK, for now the data model is simple, but when it scales for more relations, the “save code” will become a mess along you application.

In a rudimentar way, mongo has some kind of “trigger”. With that we can automate the operations to save and remove items in both sides.
lets take a look:
// "cascade triggers"
BlogPostSchema.pre('remove', function(next) { 
    // remove all commens with BlogPost id
    Comment.remove({ "BlogPost" : this._id }).exec(); 
    next();
});

CommentSchema.pre('remove', function(next) {
    // this will find locate the blog post
    BlogPost.findById(this.BlogPost).exec((error, item) => {
      // remove comment from array in the BlogPost
      var index = item.Comments.indexOf(item.Comments.find(e => e._id == this._id));
      item.Comments.splice(index, 1);
      item.save(() => { next(); });
    });
});

CommentSchema.pre('save', function(next) {
    // add comment in BlogPost array as well
    BlogPost.findById(this.BlogPost).exec((error, item) => {
      item.Comments.push(this);
      item.save(() => { next(); });
    });
});

The trick part is handle with the comments array in the blog post.

So far we had: the connection, schema, relation and “triggers”. – Now we need to register our schema and here is the beautiful part: Mongo will generate the schemas by itself and manage the changes as well. It means that I have no need to create the schema direct in the database. With the changes, the old fields in the old documents will be kept. The new documents will receive the data in the new fields.

// register schemas
const BlogPost = mongoose.model('BlogPost', BlogPostSchema);
const Comment = mongoose.model('Comment', CommentSchema);

That was the heavy stuff, now we just to setup the webserver and the API. First the webserver:

// http server 
var express    = require('express');        
var app        = express();                 

var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

And now the API:

// endpoints
var router = express.Router();   

router.get('/', function(req, res) {
    res.json({ message: 'hooray! welcome to api!' });   
});

Next steps it will define the API operations: get a list of blog posts, post a new entry in the blog, add a comment and list the comments. All of them will perform interactions with the mongo:

// POST BLOG ITEM
router.post('/blogPosts', function(req, res) {
    var entity = new BlogPost();     
    entity.Title = req.body.Title;   
    entity.Description = req.body.Description;    
    entity.save(function(error) {
        if (error)
            res.send(error);

        res.json({ message: 'created' });
    }); 
});    

// GET BLOG POSTS
router.get('/blogPosts', function(req, res) {
    BlogPost
        .find()
        .populate('Comments')
        .exec(function(err, entities) {
            if (err)
                res.send(err);

            res.json(entities);
        });
});   

// POST COMMENT
router.post('/comments', function(req, res) {
    var entity = new Comment();     
    entity.Message = req.body.Message;   
    entity.BlogPost = mongoose.Types.ObjectId(req.body.BlogPost);    
    entity.save(function(error) {
        if (error)
            res.send(error);

        res.json({ message: 'created' });
    }); 
});    

// GET COMMENTS
router.get('/comments', function(req, res) {
    Comment
        .find()
        .populate('BlogPost')
        .exec(function(err, entities) {
            if (err)
                res.send(err);

            res.json(entities);
        });
}); 

// register the routes
app.use('/', router);

We have to pay attention in the “populate()” method in the GET operations. It will allow the mongo recovery the document of the relation.

Finally we can start the server:

// starting http server...
const port = 5000; 
app.listen(port);
console.log('listening port: ' + port);

To run it, go to the prompt and type again “node app”.

And to test we can use Postman. Lets try the next steps:

Post a new blog entry:

Retrieving the blog entries:

Copy this “_id” value and try to post comments with that:

We now are able to see the “first magic”, when we try to get the comments:

In the above image we can see the “BlogPost” item populated!
And the other way is true as well:

First of all: it is just a simple demo, a simple example to test the two direction navigation among the data. It was created to test that and I tried to be as simple as possible.

When we think to build an API with NodeJS, there are vast ways to organize and structure the things to build a good architecture. And of course, in a API based app, we have calls to different endpoints to retrieve the exact portion of data needed (like one GET to blog entry and another to comments). But there are scenarios out of this world that are the need to relate docs using MongoDB. And I tried to expose one way to achieve that.

And now you know how to do that. Enjoy your experiences with NoSQL databases.

The final code for all of it is this (with 123 lines!):

// mongodb setup
const mongoose = require('mongoose');
const Schema = mongoose.Schema
mongoose.connect('<YOU CONNECTION STRING HERE!!!>');

// schemas of the documents
const BlogPostSchema = mongoose.Schema({
  Title: { type: String },
  Description: { type: String },

  // array defnition with the object reference from Comment
  Comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }] 
}); 

const CommentSchema = mongoose.Schema({
  // object reference from BlogPost
  BlogPost  : { type: mongoose.Schema.Types.ObjectId, ref: 'BlogPost' },
  Message: { type: String }
});

// "cascade triggers"
BlogPostSchema.pre('remove', function(next) { 
    // remove all commens with BlogPost id
    Comment.remove({ "BlogPost" : this._id }).exec(); 
    next();
});

CommentSchema.pre('remove', function(next) {
    // this will find locate the blog post
    BlogPost.findById(this.BlogPost).exec((error, item) => {
      // remove comment from array in the BlogPost
      var index = item.Comments.indexOf(item.Comments.find(e => e._id == this._id));
      item.Comments.splice(index, 1);
      item.save(() => { next(); });
    });
});

CommentSchema.pre('save', function(next) {
    // add comment in BlogPost array as well
    BlogPost.findById(this.BlogPost).exec((error, item) => {
      item.Comments.push(this);
      item.save(() => { next(); });
    });
});

// register schemas
const BlogPost = mongoose.model('BlogPost', BlogPostSchema);
const Comment = mongoose.model('Comment', CommentSchema);

// http server 
var express    = require('express');        
var app        = express();                 

var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// endpoints
var router = express.Router();   

router.get('/', function(req, res) {
    res.json({ message: 'hooray! welcome to api!' });   
});           

// POST BLOG ITEM
router.post('/blogPosts', function(req, res) {
    var entity = new BlogPost();     
    entity.Title = req.body.Title;   
    entity.Description = req.body.Description;    
    entity.save(function(error) {
        if (error)
            res.send(error);

        res.json({ message: 'created' });
    }); 
});    

// GET BLOG POSTS
router.get('/blogPosts', function(req, res) {
    BlogPost
        .find()
        .populate('Comments')
        .exec(function(err, entities) {
            if (err)
                res.send(err);

            res.json(entities);
        });
});   

// POST COMMENT
router.post('/comments', function(req, res) {
    var entity = new Comment();     
    entity.Message = req.body.Message;   
    entity.BlogPost = mongoose.Types.ObjectId(req.body.BlogPost);    
    entity.save(function(error) {
        if (error)
            res.send(error);

        res.json({ message: 'created' });
    }); 
});    

// GET COMMENTS
router.get('/comments', function(req, res) {
    Comment
        .find()
        .populate('BlogPost')
        .exec(function(err, entities) {
            if (err)
                res.send(err);

            res.json(entities);
        });
}); 

// register the routes
app.use('/', router);

// starting http server...
const port = 5000; 
app.listen(port);
console.log('listening port: ' + port);

GitHub

Spaki.

With more than 15 years of experience developing softwares and technologies, talking about startups, trends and innovation, today my work is focused to be CTO, Software Architect, Technical Speaker, Technical Consultant and Entrepreneur.

From Brazil, currently lives in Portugal working at https://www.farfetch.com as Software Architect, besides to keep projects in Brazil, like http://www.almocando.com.br/

Share

3 thoughts on “1 to N bidirectional relationship in the MongoDB”

  1. I have noticed you don’t monetize your blog, don’t
    waste your traffic, you can earn additional cash every month because you’ve got hi quality content.

    If you want to know how to make extra $$$, search for: Mrdalekjd methods for $$$

Leave a Reply

Your email address will not be published. Required fields are marked *