Many-to-Many relationship in MongoDB . Nodejs | Express | Mongoose
Tags: mongodb, node, database, webdev
Published: 2021-04-03 03:20:58

Recently I was working on a project (Node, Express, MongoDB, Mongoose) where I needed to create many-to-many relationships with products and categories where categories can have multiple products and products can be in multiple categories.

So I started working on it, I made it so that adding, removing, or updating products will automatically update categories too and vice-versa.

Then I thought let's see how others have done it. So I searched for it but I could not find something similar, this made me think why not share my method so others can get help from it or someone can correct me.

So here is how I did it, this is the simplified version of my full code.

###Models
First I created product and category model.
#####Product Model
```javascript
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const productSchema = new Schema({
    name:           { type: String, required: true},
    price:          { type: Number, required: true, min: 0 },
    categories:     [{ type: mongoose.Types.ObjectId, ref: 'Category' }],
});

module.exports = new mongoose.model('Product', productSchema);
```
Here you can see my product model has three simple fields first is the name of the product, second is the price of the product and third is the array of categories in which this product belongs.

#####Category Model
```javascript
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const categorySchema = new Schema({
    name:       { type: String, required: true },
    products:   [{ type: mongoose.Types.ObjectId, ref: 'Product' }],
});
module.exports = new mongoose.model('Category', categorySchema);
```
The category model is also a simple one with two fields first is the name of the category and the second is the array of all products in this category.

###Routes
I will show you only three routes Create, Update and Delete route for simplicity.

#####1. Create Route
Suppose we want to add a new product iPhone 12 and add it to three categories Mobile, Smartphone, Electronics. So we will send our request with the following data.
```JSON
{
    product: {
        "name": "iPhone 12",
        "price": "1200",
        "categories": ["606563248d90d1833f3cda0b", "606563334294906c9a9b9dfe", "6065633736dee94dfae613d7"]
    }
}
```
Here categories are the id of categories in which we want to add. our product.
To handle this request I created the following route.
```javascript
router.post('/', async function (req, res) {
  const { product } = req.body;
  
  const newProduct = await Product.create(product);

  await Category.updateMany({ '_id': newProduct.categories }, { $push: { products: newProduct._id } });

  return res.send(newProduct);
});
```
First I extract product data from req.body.
`const { product } = req.body;`

Then I created a new product from that data.
`const newProduct = await Product.create(product);`

After creating the product I use [updateMany()](https://mongoosejs.com/docs/api.html#model_Model.updateMany) to find all categories in this product and update them to include this product.
`await Category.updateMany({ '_id': newProduct.categories }, { $push: { products: newProduct._id } });`

#####Delete Route
```javascript
router.delete('/:id', async function (req, res) {
  const _id = req.params.id;
  const product = await Product.findOne({ _id });

  await product.remove();

  await Category.updateMany({ '_id': product.categories }, { $pull: { products: product._id } });

  return res.redirect(product);

});
```
Delete route was also straightforward as create route. 
First I find the product by id then remove it,
`await Product.findOne({ _id });`
`await product.remove();`

and then update all categories in this product to remove this product.
`await Category.updateMany({ '_id': product.categories }, { $pull: { products: product._id } });`

#####Update Route
The update route was a little bit tricky. Because here categories of product can be changed so we have to know which categories were added and which were removed. To do this I created this function.
```javascript
function difference(A, B) {
  const arrA = Array.isArray(A) ? A.map(x => x.toString()) : [A.toString()];
  const arrB = Array.isArray(B) ? B.map(x => x.toString()) : [B.toString()];

  const result = [];
  for (const p of arrA) {
    if (arrB.indexOf(p) === -1) {
      result.push(p);
    }
  }

  return result;
}
```
This function basically takes two arrays A, B and returns a new array of items that are present in array A but not in array B. Something like this.
```javascript
const arrA = ['a', 'b', 'c', 'd'];
const arrB = ['c', 'd', 'e', 'f'];

difference(arrA, arrB);
// Returns: ['a', 'b'] present in arrA but not in arrB

difference(arrB, arrA);
// Returns: ['e', 'f'] present in arrB but not in arrA
```
I used it to compare old and new categories to know which categories got removed and which were added.

So to update product I created this route.
```javascript
router.put('/:id', async function (req, res) {
  const _id = req.params.id;
  const { product } = req.body;
  const newCategories = product.categories || [];

  const oldProduct = await Product.findOne({ _id });
  const oldCategories = oldProduct.categories;

  Object.assign(oldProduct, product);
  const newProduct = await oldProduct.save();

  const added = difference(newCategories, oldCategories);
  const removed = difference(oldCategories, newCategories);
  await Category.updateMany({ '_id': added }, { $addToSet: { products: foundProduct._id } });
  await Category.updateMany({ '_id': removed }, { $pull: { products: foundProduct._id } });

  return res.send(newProduct);
});
```
Here first I took id from `req.params.id` and product update date from `req.body` then I took `newCategories` from product.

After that I find the product by id and took `oldCategories` from them so I can compare them with new categories and determine which categories were added and which were removed.
Then I assigned all properties from the product to the old product and save it. 

After updating the product I used `difference()` function with `newCategories` and `oldCategories` to get the categories those were added and those were removed and then I used two `updateMany` operation on categories to add this product to `added` categories and remove it from `removed` categories.


> ###Categories Route
> categories routes were the same as product routes I just used category in place of product and vice-versa.