MongoDB Many-to-Many Relationship with Mongoose examples

In this tutorial, I will show you how to deal with MongoDB Many to Many Relationship which is an important and complicated Relationship that you will use in most database structures. Then we’re gonna use Mongoose library to make a MongoDB Many to Many Relationship example.

Related Posts:
MongoDB One-to-One relationship tutorial with Mongoose example
MongoDB One-to-Many Relationship tutorial with Mongoose examples

Node.js, Express & MongoDb: Build a CRUD Rest Api example
Node.js + MongoDB: User Authentication & Authorization with JWT


Model Many-to-Many Relationships in MongoDB

Think about a Tutorial Blog with the relationship between Tutorial and Tag that goes in both directions:

  • A Tutorial can have many Tags
  • A Tag can point to many Tutorials

mongodb-many-to-many-relationship-mongoose-example

We call them Many-to-Many relationships.

Let’s explore how they look like after we implement two way of modeling these datasets.

Embedded Data Models (Denormalization)

We can denormalize data into a denormalized form simply by embedding the related documents right into the main document.

Let’s put all the relevant data about Tags right in one Tutorial document without the need to separate documents, collections, and IDs.

// Tutorial
{
  _id: "5db579f5faf1f8434098f123"
  title: "Tut #1",
  author: "bezkoder"
  tags: [
			  {
			    name: "tagA",
			    slug: "tag-a"
			  },
			  {
			    name: "tagB",
			    slug: "tag-b"
			  }
			]
}

// Tag
{
  _id: "5db579f5faf1f84340abf456"
  name: "tagA",
  slug: "tag-a"
  tutorials: [
			  {
			    title: "Tut #1",
			    author: "bezkoder"
			  },
			  {
			    title: "Tut #2",
			    author: "zkoder"
			  }
			]
}

Reference Data Models (Normalization)

Now for MongoDB referenced form – ‘normalized’ means ‘separated’. We need to separate documents, collections, and IDs.

Because Tutorials and Tags are all completely different document, the Tutorial need a way to know which Tags it has, so does a Tag that will know which Tutorials contain it. That’s why the IDs come in.

We’re gonna use Tutorials’ IDs for references on Tag document.

// Tags
// tagA: [Tut #1, Tut #2]
{
  _id: "5db57a03faf1f8434098ab01",
  name: "tagA",
  slug: "tag-a",
  tutorials: [ "5db579f5faf1f8434098f123", "5db579f5faf1f8434098f456" ]
}

// tagB: [Tut #1]
{
  _id: "5db57a04faf1f8434098ab02",
  name: "tagB",
  slug: "tag-b",
  tutorials: [ "5db579f5faf1f8434098f123" ]
}

You can see that in the Tag document, we have an array where we stored the IDs of all the Tutorials so that when we request a Tag data, we can easily identify its Tutorials.
This type of referencing is called Child Referencing: the parent (Tag) references its children (Tutorials).

Nown how about Parent Referencing?
Each child document keeps a reference to the parent element.

We have Tutorials get references to its Tags’ Ids.

// Tutorial
// Tut #1: [tagA, tagB]
{
  _id: "5db579f5faf1f8434098f123"
  title: "Tut #1",
  author: "bezkoder"
  tags: [ "5db57a03faf1f8434098ab01", "5db57a04faf1f8434098ab02" ],
}

// Tut #2: [tagA]
{
  _id: "5db579f5faf1f8434098f456"
  title: "Tut #2",
  author: "zkoder"
  tags: [ "5db57a03faf1f8434098ab01" ],
}

What we’ve just done is called Two-way Referencing where Tags and Tutorials are connected in both directions:
– In each Tag, we keep references to all Tutorials that are tagged.
– In each Tutorial, we also keep references to its Tags.

References or Embedding for MongoDB Many-to-Many Relationships

For Embedded Data Models, you can see that we can get all the data about Tutorial with its Tags (or Tag with its Tutorials) at the same time, our application will need few queries to the database. It will increase our performance.

But when the data become larger and larger, duplicates is inevitable.
And the risk is so serious in case we want to update document, even just a field, we have to find and update two place.

For example, if you want to change the name of tagA, you must have to change not only that Tag’s document but also find the Tutorial that contains the Tag, find that Tag exactly, then update the field. So terrible!

Hence, with Many-to-Many relationship, we always use Data References or Normalizing the data.

Mongoose Many-to-Many Relationship example

Setup Node.js App

Install mongoose with the command:

npm install mongoose

Create project structure like this:


src

models

Tag.js

Tutorial.js

index.js

server.js

package.json


Open server.js, we import mongoose and connect the app to MongoDB database.

const mongoose = require("mongoose");

mongoose
  .connect("mongodb://localhost/bezkoder_db", {
    useNewUrlParser: true,
    useUnifiedTopology: true
  })
  .then(() => console.log("Successfully connect to MongoDB."))
  .catch(err => console.error("Connection error", err));

Next, export the models in index.js.

module.exports = {
  Tag: require("./Image"),
  Tutorial: require("./Tutorial")
};

That’s the first step, now we’re gonna create 2 main models: Tag & Tutorial, then use mongoose to interact with MongoDB database.

Define Mongoose data models

In models/Tag.js, define Tag with 3 fields: name, slug, tutorials

const mongoose = require("mongoose");

const Tag = mongoose.model(
  "Tag",
  new mongoose.Schema({
    name: String,
    slug: String,
    tutorials: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: "Tutorial"
      }
    ]
  })
);

module.exports = Tag;

In the code above, we set each item’s type of tutorials array to ObjectId and ref to Tutorial. Why we do it?

The Tutorial has only ObjectId in tutorials array. ref helps us get full fields of Tutorial when we call populate() method. I will show you how to do it later.

In models/Tutorial.js, define Tutorial with 3 fields: title, author, tags

const mongoose = require("mongoose");

const Tutorial = mongoose.model(
  "Tutorial",
  new mongoose.Schema({
    title: String,
    author: String,
    tags: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: "Tag"
      }
    ]
  })
);

module.exports = Tutorial;

Use Mongoose Model functions to create Documents

We’re gonna define some functions:

  • createTutorial
  • createTag
  • addTagToTutorial
  • addTutorialToTag

Open server.js, add the code below:

const mongoose = require("mongoose");
const db = require("./models");

const createTutorial = function(tutorial) {
  return db.Tutorial.create(tutorial).then(docTutorial => {
    console.log("\n>> Created Tutorial:\n", docTutorial);
    return docTutorial;
  });
};

const createTag = function(tag) {
  return db.Tag.create(tag).then(docTag => {
    console.log("\n>> Created Tag:\n", docTag);
    return docTag;
  });
};

const addTagToTutorial = function(tutorialId, tag) {
  return db.Tutorial.findByIdAndUpdate(
    tutorialId,
    { $push: { tags: tag._id } },
    { new: true, useFindAndModify: false }
  );
};

const addTutorialToTag = function(tagId, tutorial) {
  return db.Tag.findByIdAndUpdate(
    tagId,
    { $push: { tutorials: tutorial._id } },
    { new: true, useFindAndModify: false }
  );
};

const run = async function() {
  var tut1 = await createTutorial({
    title: "Tut #1",
    author: "bezkoder"
  });

  var tagA = await createTag({
    name: "tagA",
    slug: "tag-a"
  });

  var tagB = await createTag({
    name: "tagB",
    slug: "tag-b"
  });

  var tutorial = await addTagToTutorial(tut1._id, tagA);
  console.log("\n>> tut1:\n", tutorial);

  var tag = await addTutorialToTag(tagA._id, tut1);
  console.log("\n>> tagA:\n", tag);

  tutorial = await addTagToTutorial(tut1._id, tagB);
  console.log("\n>> tut1:\n", tutorial);

  tag = await addTutorialToTag(tagB._id, tut1);
  console.log("\n>> tagB:\n", tag);

  var tut2 = await createTutorial({
    title: "Tut #2",
    author: "zkoder"
  });

  tutorial = await addTagToTutorial(tut2._id, tagB);
  console.log("\n>> tut2:\n", tutorial);

  tag = await addTutorialToTag(tagB._id, tut2);
  console.log("\n>> tagB:\n", tag);
};

mongoose
  .connect("mongodb://localhost/bezkoder_db", {
    useNewUrlParser: true,
    useUnifiedTopology: true
  })
  .then(() => console.log("Successfully connect to MongoDB."))
  .catch(err => console.error("Connection error", err));

run();

Run the app with command: node src/server.js.
You can see the result in Console.

Successfully connect to MongoDB.

>> Created Tutorial:
 { tags: [],
  _id: 5e417363316cab53182888d8,
  title: 'Tut #1',
  author: 'bezkoder',
  __v: 0 }

>> Created Tag:
 { tutorials: [],
  _id: 5e417363316cab53182888d9,
  name: 'tagA',
  slug: 'tag-a',
  __v: 0 }

>> Created Tag:
 { tutorials: [],
  _id: 5e417363316cab53182888da,
  name: 'tagB',
  slug: 'tag-b',
  __v: 0 }

>> tut1:
 { tags: [ 5e417363316cab53182888d9 ],
  _id: 5e417363316cab53182888d8,      
  title: 'Tut #1',
  author: 'bezkoder',
  __v: 0 }

>> tagA:
 { tutorials: [ 5e417363316cab53182888d8 ],
  _id: 5e417363316cab53182888d9,
  name: 'tagA',
  slug: 'tag-a',
  __v: 0 }

>> tut1:
 { tags: [ 5e417363316cab53182888d9, 5e417363316cab53182888da ],
  _id: 5e417363316cab53182888d8,
  title: 'Tut #1',
  author: 'bezkoder',
  __v: 0 }

>> tagB:
 { tutorials: [ 5e417363316cab53182888d8 ],
  _id: 5e417363316cab53182888da,
  name: 'tagB',
  slug: 'tag-b',
  __v: 0 }

>> Created Tutorial:
 { tags: [],
  _id: 5e417363316cab53182888db,
  title: 'Tut #2',
  author: 'zkoder',
  __v: 0 }

>> tut2:
 { tags: [ 5e417363316cab53182888da ],
  _id: 5e417363316cab53182888db,
  title: 'Tut #2',
  author: 'zkoder',
  __v: 0 }

>> tagB:
 { tutorials: [ 5e417363316cab53182888d8, 5e417363316cab53182888db ],
  _id: 5e417363316cab53182888da,
  name: 'tagB',
  slug: 'tag-b',
  __v: 0 }

Populate referenced documents

You can see that the tutorials/tags array field contains reference IDs.
This is the time to use populate() function to get full data. Let’s create two functions:

  • getTutorialWithPopulate
  • getTagWithPopulate
const getTutorialWithPopulate = function(id) {
  return db.Tutorial.findById(id).populate("tags");
};

const getTagWithPopulate = function(id) {
  return db.Tag.findById(id).populate("tutorials");
};

const run = async function() {
  // ...

  // add this
  tutorial = await getTutorialWithPopulate(tut1._id);
  console.log("\n>> populated tut1:\n", tutorial);

  tag = await getTagWithPopulate(tag._id);
  console.log("\n>> populated tagB:\n", tag);
};

Run again, the result will look like this-

>> populated tut1:
 { tags:
   [ { tutorials: [Array],
       _id: 5e417363316cab53182888d9,
       name: 'tagA',
       slug: 'tag-a',
       __v: 0 },
     { tutorials: [Array],
       _id: 5e417363316cab53182888da,
       name: 'tagB',
       slug: 'tag-b',
       __v: 0 } ],
  _id: 5e417363316cab53182888d8,
  title: 'Tut #1',
  author: 'bezkoder',
  __v: 0 }

>> populated tagB:
 { tutorials:
   [ { tags: [Array],
       _id: 5e417363316cab53182888d8,
       title: 'Tut #1',
       author: 'bezkoder',
       __v: 0 },
     { tags: [Array],
       _id: 5e417363316cab53182888db,
       title: 'Tut #2',
       author: 'zkoder',
       __v: 0 } ],
  _id: 5e417363316cab53182888da,
  name: 'tagB',
  slug: 'tag-b',
  __v: 0 }

If you don’t want to get _id & __v in the arrays result, just add second parameters to populate() function.

const getTutorialWithPopulate = function(id) {
  return db.Tutorial.findById(id).populate("tags", "-_id -__v -tutorials");
};

const getTagWithPopulate = function(id) {
  return db.Tag.findById(id).populate("tutorials", "-_id -__v -tags");
};

The result is different now.

>> populated tut1:
 { tags:
   [ { name: 'tagA', slug: 'tag-a' },
     { name: 'tagB', slug: 'tag-b' } ],
  _id: 5e417363316cab53182888d8,
  title: 'Tut #1',
  author: 'bezkoder',
  __v: 0 }

>> populated tagB:
 { tutorials:
   [ { title: 'Tut #1', author: 'bezkoder' },
     { title: 'Tut #2', author: 'zkoder' } ],
  _id: 5e417363316cab53182888da,
  name: 'tagB',
  slug: 'tag-b',
  __v: 0 }

Conclusion

Today we’ve learned many things about MongoDB Many-to-Many relationship and implement the example in a Node.js app using Mongoose.

You will also know 3 criteria to choose Referencing or Embedding for improving application performance in the post:
MongoDB One-to-Many Relationship tutorial with Mongoose examples

Happy learning! See you again.

Source Code

You can find the complete source code for this example on Github.

Further Reading

Fullstack CRUD App:
– MEVN: Vue.js + Node.js + Express + MongoDB example
– MEAN:
Angular 8 + Node.js + Express + MongoDB example
Angular 10 + Node.js + Express + MongoDB example
Angular 11 + Node.js + Express + MongoDB example
Angular 12 + Node.js + Express + MongoDB example
Angular 13 + Node.js + Express + MongoDB example
Angular 14 + Node.js + Express + MongoDB example
Angular 15 + Node.js + Express + MongoDB example
Angular 16 + Node.js + Express + MongoDB example
– MERN: React + Node.js + Express + MongoDB example

18 thoughts to “MongoDB Many-to-Many Relationship with Mongoose examples”

  1. Hi.

    Let’s say we have a Product and an Order table.
    > An Order can have many Products.
    > A Products can appear in may Orders.

    So we are in a many to many relationship.

    If I follow your method and if my Product is ordered 1 million times, then it means the Product will hold an array of Orders containing 1 million ids.

    Is that right?
    Is it a problem or not?

    If yes, what do you suggest in that situation?
    Great tutorials, though, thank you very much for your explanation.

    Kind regards

    1. Hi, do you need to query all orders containing specific product, and why?
      Is there any problem if you implement one-to-many relationship: one Order – many Products?

      If you use MySQL or PostgreSQL for example, it is many-to-many. But NoSQL is different from SQL.
      We need to think about that difference.

  2. Awesome tutorial, my project is using these functions and this tutorial confirms my many-to-many associations. Thank you!

  3. Thank you for tutorial
    In index.js we should have it as:
    module.exports = {
    Tag: require(“./Tag”),
    Tutorial: require(“./Tutorial”)
    };

  4. Hi, thanks for the tutorial.
    It is very useful.

    In my case I add and remove many tags from tutorials and tutorials from tags at once.
    I use find(), then replace the array with the new array and save()

    For consistency I have this “pre middlewares” that auto add/remove tags/tutorials:

    TutorialSchema.pre('save', function(next){
    
      // remove tutorials from tags
      this.model('Tag').updateMany({ _id: { $nin: this.tags } }, { $pull: { tutorials: this._id} } ).exec()
    
      // add tutorials to tags:
      this.model('Tag').updateMany({ _id: { $in: this.tags } }, { $addToSet: { tutorials: this._id } } ).exec()
    
      next();
    })
    TagSchema.pre('save', function(next){
    
      // remove tags from tutorial:
      this.model('Tutorial').updateMany({ _id: { $nin: this.tutorials } }, { $pull: { tags: this._id} } ).exec()
    
      // add tags to tutorial:
      this.model('Tutorial').updateMany({ _id: { $in: this.tutorials } }, { $addToSet: { tags: this._id } } ).exec()
    
      next();
    });
    
  5. Thank you for the instructions to create many to many references in mongoDB,
    can u suggest some good books regarding mongoDB mongoose.

  6. You’ve explained the MongoDB many to many relationship clearly. It helps me alot.
    Thanks for this Mongoose tutorial.

  7. Hello,

    Firstly, thanks for the tutorial. It was very helpful to me.

    I just have a hard time understanding why Tag is the parent and Tutorial is the child.
    So, when I read “…Tutorial need a way to know which Tags it has” for me this means that Tutorial is the parent, that has Tags children.

    I’m just asking this because it confused me who I should consider for parent or child in my code 😕

    Thanks

    1. Hi, this is two-way referencing, so you can think about the One-to-Many relationship between them works both ways:

      • one Tutorial has many Tags (Tutorial: parent, Tags: children)
      • one Tag can point to many Tutorials (Tag: parent, Tutorials: children)
  8. Thank you so much for these write-ups, they’re are as explanatory and kind of more insightful as video tutorials. I’m learning react, but you’ll inspire me to learn VueJs.
    Thanks so much

Comments are closed to reduce spam. If you have any question, please send me an email.