0

I have a document like this one:

{
 _id:ObjectId('111'),
 products:[
  {
   _id:ObjectId('aaa'),
   quantity:2,
   price:800
  }
 ]
}

I want to update price field by multiplying it with quantity field ie (2 * 800) of which the result gets updated/assigned to price. (as for this example price gets updated to 1600).

Document after Update :

{
 _id:ObjectId('111'),
 products:[
  {
   _id:ObjectId('aaa'),
   quantity:2,
   price:1600  //the updated field by multiplying the initial 800 * 2
  }
 ]
}

My query for selecting is like this below:

    Shop.findOneAndUpdate(
        { "_id": '111, "products._id": 'aaa' }
    )

How can I achieve this?

4
  • it's a multi step process. Query the main document, get the array element, update the array element, the save the main document (which includes array too) Commented May 10, 2020 at 2:36
  • Did you check my answer? Commented May 12, 2020 at 20:07
  • @SuleymanSah your solution looks quite impressive... Is it less expensive than using $ operators and looping the products array like how whoami suggested in the second answer? Commented May 18, 2020 at 10:07
  • My answer requires findById which causes an extra db access, but since finding by id is very fast, it has almost no performance penalty. I have no idea about the performance of solution of whoami. Commented May 18, 2020 at 10:13

2 Answers 2

3

As @Casey suggested in the comments, you can do this in multi step, find the shop, find the product, change product price, save the shop.

router.patch("/shops/:shopId/:productId", async (req, res) => {
  const { shopId, productId } = req.params;

  let shop = await Shop.findById(shopId);

  if (!shop) return res.status(400).send("Shop not found");

  const productIndex = shop.products.findIndex((p) => p._id.toString() === productId);

  if (productIndex < 0) return res.status(400).send("Product not found in shop");

  let product = shop.products[productIndex];

  product.price *= product.quantity;

  shop = await shop.save();

  res.send(shop);
});

Let's say you have this existing shop with two products:

{
    "_id": "5eb85ab17c2bfb3e2cfc15d0",
    "products": [
        {
            "_id": "5eb85ab17c2bfb3e2cfc15d2",
            "quantity": 2,
            "price": 800
        },
        {
            "_id": "5eb85ab17c2bfb3e2cfc15d1",
            "quantity": 3,
            "price": 500
        }
    ]
}

If you want to update the price with the "_id": "5eb85ab17c2bfb3e2cfc15d2", we send a patch request to the url http://your base url/shops/5eb85ab17c2bfb3e2cfc15d0/5eb85ab17c2bfb3e2cfc15d2

The output will be like this:

{
    "_id": "5eb85ab17c2bfb3e2cfc15d0",
    "products": [
        {
            "_id": "5eb85ab17c2bfb3e2cfc15d2",
            "quantity": 2,
            "price": 1600  => UPDATED
        },
        {
            "_id": "5eb85ab17c2bfb3e2cfc15d1",
            "quantity": 3,
            "price": 500
        }
    ]
}
Sign up to request clarification or add additional context in comments.

2 Comments

@Amani I will be happy if you can at least upvote this answer, it answers your question.
I have upvoted it. Your solution looks amazing, finding an index, updating the value and save it. With no looping the products array, I really think that this is the way to go
1

On MongoDB version >= 4.2 as you can execute aggregation-pipeline in updates, try below query :

Shop.update(
  /** Remember to convert input strings to type `ObjectId()` prior to querying */
  { _id: ObjectId("111"), "products._id": ObjectId("aaa") },
  /** This aggregation pipeline will re-create `products` array,
   *  if condition is met for an object then price will be multiplied & price field is merged to original object & pushed back to `products` array, 
   *  if condition is not met actual object is pushed back to array */
  [
    {
      $set: {
        products: {
          $map: {
            input: "$products",
            in: {
              $cond: [
                { $eq: ["$$this._id", ObjectId("aaa")] },
                {
                  $mergeObjects: [ "$$this", { price: { $multiply: ["$$this.quantity", "$$this.price"] }}]
                },
                "$$this"
              ]
            }
          }
        }
      }
    }
  ]
);

4 Comments

Thanks for the answer... But instead of looping using $map, since _id 'aaa' is already found from our filter in products._id": ObjectId("aaa") (hence we can get the specific item in products array), isn't there just a simple way of doing this using $set? something like { "$set": { "$price":{$multiply: ["$$this.quantity", "$$this.price"]} } } or maybe { "$set": { "$price":{$multiply: ["$quantity", "$price"]} } } ?
@Amani : what you’re talking about is ‘$’ positional operator unfortunately which you can’t use in this case..
Got it. I have tried it and it works greatly. (though looping on products looked somewhat expensive if the array has so many items). I've upvoted your answer.
@Amani : I believe there is no other better way to do it & it's better than doing two DB calls - one to read nd other to update, I guess you might end-up doing almost the same even in code. So better take advantage of new option of using aggregation pipeline in updates..

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.