DEV Community

loading...
Cover image for Many-to-Many relationship in MongoDB . Nodejs | Express | Mongoose

Many-to-Many relationship in MongoDB . Nodejs | Express | Mongoose

nehalahmadkhan profile image Nehal Ahmad ・4 min read

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
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);
Enter fullscreen mode Exit fullscreen mode

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
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);
Enter fullscreen mode Exit fullscreen mode

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.

{
    product: {
        "name": "iPhone 12",
        "price": "1200",
        "categories": ["606563248d90d1833f3cda0b", "606563334294906c9a9b9dfe", "6065633736dee94dfae613d7"]
    }
}
Enter fullscreen mode Exit fullscreen mode

Here categories are the id of categories in which we want to add. our product.
To handle this request I created the following route.

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);
});
Enter fullscreen mode Exit fullscreen mode

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() 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
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);

});
Enter fullscreen mode Exit fullscreen mode

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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

pic
Editor guide