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
Contents
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
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
– MERN: React + Node.js + Express + MongoDB example
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
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.
Awesome tutorial, my project is using these functions and this tutorial confirms my many-to-many associations. Thank you!
Thank you for tutorial
In index.js we should have it as:
module.exports = {
Tag: require(“./Tag”),
Tutorial: require(“./Tutorial”)
};
Very impressive MongoDB & Mongoose tutorial!
How do I update the tutorials data if any one of the tag is deleted?
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:
Thank you so much! Your code could help other people 🙂
This was a wonderful tutorial. I have learnt a lot on these posts. Thank you.
thanx so much for such a great tutorial 🙂
Thank you for the instructions to create many to many references in mongoDB,
can u suggest some good books regarding mongoDB mongoose.
Hi, you can read this book: MongoDB in Action.
You’ve explained the MongoDB many to many relationship clearly. It helps me alot.
Thanks for this Mongoose tutorial.
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
Hi, this is two-way referencing, so you can think about the One-to-Many relationship between them works both ways:
I like the way you explain every line of codes
Thanks
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
Hi, I’m so happy to know that my work inspires you. Wish you all the best!