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);
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);
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"]
}
}
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);
});
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);
});
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;
}
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
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);
});
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.
Top comments (3)
what about if we want to add a subCategory , i had lot of problems with that can you help please
I'm currently learning database modeling in Mongo and this depends a lot on your project, but assuming that the relationship between the category and subcategory is One-to-Few (one category and a few subcategories) you can embed the subcategories into the category document
lots of confusion
product will comes under category so
why create product before create category
product will comes under category