📖
Maissen's Grimoire
  • Maissen's Grimoire
  • Html and css grimoire
    • HTML5 Periodical Table
    • HTML Cheat Sheet
    • CSS Cheatsheets
  • Javascript Grimoire
    • JavaScript Cheat Sheet
      • Javascript Array in depth
      • Tagged Template Literals
      • Guard Clauses - The Best Way To Write Complex Conditional Logic
      • JavaScript Optional Chaining
      • JavaScript Null Coalesce
      • What Are Magic Numbers And Why Are They Bad
      • ES6/ECMAScript2015 Cheatsheet
      • First-class and Higher Order Functions: Effective Functional JavaScript
    • Useful JavaScript Tips, Tricks and Best Practices
    • Bits of code
    • Useful JavaScript libraries
      • Components
      • Animation
      • Maps
      • Helpers
      • Presentations
      • Charts
      • Games
      • Audio
      • Images
      • Video
    • Js the right way
  • Angular Grimoire
    • Angular doc
    • Getting Started
    • Angular clean architecture
    • Angular Cheat Sheet
    • TypeScript Cheat Sheet
    • My Favorite Tips and Tricks in Angular
    • NgRx: tips & tricks
    • Bits of code
      • Execute Multiple HTTP Requests in Angular
      • Authentification
        • Angular 8 JWT Authentication with HttpInterceptor and Router
      • Integrations
        • Spring Boot
          • Rest Example
            • Angular,Spring Boot,Spring Data and Rest Example(CRUD)
          • Authentification
            • Angular, Spring Boot: JWT Authentication with Spring Security example
            • Angular Spring Boot Security Oauth2
              • Spring Boot OAUTH2 Role-Based Authorization
              • Spring Boot Security Google Oauth
              • Spring Security OAuth2 User Registration
    • Most used dependency
  • Node Grimoire
    • Express.js 4 Cheatsheet
    • Useful Dependencies
    • How To Use And Write Express Middleware
    • Node.js with SQL databases
      • Node.js Token Based Authentication & Authorization example
      • Node.js Rest APIs example with Express, Sequelize & MySQL
      • Node.js Express & PostgreSQL: CRUD Rest APIs example with Sequelize
      • Sequelize
        • Sequelize Many-to-Many Association example – Node.js & MySQL
        • Sequelize One-to-Many Association example with Node.js & MySQL
    • Node.js with NOSQL databases
      • Node.js + MongoDB: User Authentication & Authorization with JWT
      • Node.js, Express & MongoDb: Build a CRUD Rest Api example
      • MongoDB One-to-One relationship tutorial with Mongoose example
      • MongoDB One-to-Many Relationship tutorial with Mongoose examples
      • MongoDB Many-to-Many Relationship with Mongoose examples
  • Upload files
    • How to upload multiple files in Node.js
    • Upload & resize multiple images in Node.js using Express, Multer, Sharp
    • Upload/store images in MySQL using Node.js, Express & Multer
    • How to upload/store images in MongoDB using Node.js, Express & Multer
  • React Grimoire
    • React Doc
    • React Grimoire
    • React Cheat Sheet
  • spring boot Grimoire
    • Getting started
    • Spring Boot, Spring Data JPA – Rest CRUD API example
    • Spring Boot Token based Authentication with Spring Security & JWT
  • Mongo Grimoire
    • MongoDb-Mongoose Cheat Sheet
  • Development tools
    • Design Patterns
  • maissen_grimoire
Powered by GitBook
On this page
  • Model Many-to-Many Relationships in MongoDB
  • References or Embedding for MongoDB Many-to-Many Relationships
  • Mongoose Many-to-Many Relationship example

Was this helpful?

  1. Node Grimoire
  2. Node.js with NOSQL databases

MongoDB Many-to-Many Relationship with Mongoose examples

PreviousMongoDB One-to-Many Relationship tutorial with Mongoose examplesNextUpload files

Last updated 4 years ago

Was this helpful?

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?

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 }

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

populate()