MongoDB.live, free & fully virtual. June 9th - 10th. Register Now

Need help to understand transactions a little bit better

const db = mongoose.connection

  let orderTotal = 0

  //try with async iterators

  let session = await db.startSession()

  try {

    session.startTransaction()

    for await (const item of req.body) {

      for await (const val of item.values) {

        await ProdIns.findOneAndUpdate({

          SKU: item.SKU,

          categoryOptId: val.optionId,

          categoryOptVal: val._id,

          stockQty: { $gt: item.qty }

        }, {$inc:{stockQty:-item.qty}}, { session: session })

        await ProdIns.findByIdAndUpdate(val._id, { $inc: { stockQty: -item.qty } }

        )

      }

      await Cart.findByIdAndUpdate(item._id, { $set: { completed: true } }, { session: session })

    }

    await session.commitTransaction()

    session.endSession()

    res.status(201).send()

  } catch (e) {

    console.log(e)

    await session.abortTransaction()

    res.status(500).send()

  }

In the above snippet I clearly know that I dont have any matching doc in ProdIns model that has stockQty field $gt: item.qty; In this case isnt it obvious that my transaction would get aborted? if not then why? And how to make it abort if ProdIns collection does not have any matching document that has stockQty $gt item.qty.

Also I am using for loop to iterate through user’s cart items array. for every instance of the Cart items array my intent is to iterates through the variants(values) of that item and deduce the number of quantity user desires to order from ProdIns collection. Is it viable option to use async iteration? Any better alternative? Any Similar example you can give? how do you take your cart items and convert them into orders any tips on that please?

Hey @shorif_shakil

For you first question about the transaction not aborting when your prodIn findOneAndUpdate does not find. The transaction will only abort when you throw an error and it is caught as your code reads. If you want to abort the entire transaction when no document found then you would have to check for that and call abortTransaction. Keep in mind that falling to find is not an error. Just return undefined

Your next question about iterating a loop with for of await. If you can I think it may be better to loop through your cart and create an array of bulk write objects. Then once you have all the updates, inserts, etc you send then to mongodb with a bulk write.
https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/

1 Like

hey Sean Campbell, I appreciate your support. but I need to have the commit all or nothing feature of transactions. I tried to throw an error from a callback but that doesn’t seem to work.

const db = mongoose.connection

  let orderTotal = 0

  //try with async iterators

  let session = await db.startSession()

  try {

    session.startTransaction()

    for await (const item of req.body) {

      for await (const val of item.values) {

        ProdIns.findOneAndUpdate({

          SKU: item.SKU,

          categoryOptId: val.optionId,

          categoryOptVal: val._id,

          stockQty: { $gt: item.qty }

        }, { $inc: { stockQty: -item.qty } }, { session: session, rawResult: true }, (err, doc, res) => {

          if (err) {

            throw new Error("Could not find")

          } else if (!doc.lastErrorObject.updateExisting) {

            throw new Error("Could not update")

          }

        })

      }

      await Cart.findByIdAndUpdate(item._id, { $set: { completed: true } }, { session: session })

    }

    await session.commitTransaction()

    session.endSession()

    res.status(201).send()

  } catch (e) {

    console.log(e)

    console.log("aborting")

    await session.abortTransaction()

    res.status(500).send()

  }

///////////////////////////////////
this time it returns the following error
//////////////////

events.js:292
api_1     |       throw er; // Unhandled 'error' event
api_1     |       ^
api_1     |
api_1     | Error: Could not update
api_1     |     at /usr/app/router/functions/OrderFunc.js:36:19
api_1     |     at /usr/app/node_modules/mongoose/lib/model.js:4849:16
api_1     |     at /usr/app/node_modules/mongoose/lib/model.js:4849:16
api_1     |     at /usr/app/node_modules/mongoose/lib/helpers/promiseOrCallback.js:24:16
api_1     |     at /usr/app/node_modules/mongoose/lib/model.js:4872:21
api_1     |     at /usr/app/node_modules/mongoose/lib/query.js:4379:11
api_1     |     at /usr/app/node_modules/kareem/index.js:135:16
api_1     |     at processTicksAndRejections (internal/process/task_queues.js:79:11)
api_1     | Emitted 'error' event on Function instance at:

/////////
with the previous await syntax I dont think that I have an option to check rawResult before session.commitTransaction(). Can you please be kind enough to assist me further?

Your previous syntax did not need to be changed. It was good. just add in this

const updatedItem = await ProdIns.findOneAndUpdate({
...
})

if (!updateItem) {
throw new Error('Missing item')
}

I would just suggest to try and avoid making calls to the database in a loop. So I would recommend that you do a bulk write and if any of those fails to find then throw the error to abort the transaction. Plus I would try to see where you can make changes so that you are not calling findOneAndUpdate twice on the same collection. Does this seem the most efficient to you?

Hey @Natac13
This creates the first issue transaction commits despite there is no doc matching to the condition passed to findOneAndUpdate. My intention is to iterate each document in cartItems collection and reduce matching prodIns.qty(if exists) by cartItems.qty and then commit if every reduction in prodIns agains every Item in cartItems is successful. Do you think that bulkwrite is the best way for this? Can you provide any example of using transaction sessions with bulkWrite?

A very quick idea could be

const db = mongoose.connection

  let orderTotal = 0

  //try with async iterators

  let session = await db.startSession()

  // from body
  const cartItems = [
    { 
      _id: 'mongodb1234Id',
      SKU: 'UO-1223',
      qty: 3
    },
    { 
      _id: 'mongodb5678Id',
      SKU: 'UO-4556',
      qty: 1
    }
  ] 

  try {

    session.startTransaction()
    const bulkWrites = cartItems.map((item) => {
      return {
        updateOne: {
          filter: { _id: item._id, stockQty: { $gt: item.qty } }, // or however you want to find them
          update: { $inc: { stockQty: -item.qty } },
        }
      }
    })

    const result = await ProdIns.bulkWrite(bulkWrites, { session })

    if (result.nMatched !== cartItems.length) {
      session.abortTransaction()
    }

    // else continue

  } catch (err) {

    session.abortTransaction()
  }

Please forgive me as I do not know how your data is structured. And I still do not understand why you have to make 2 calls to update the ProdIns collection right after each other.

1 Like

@Natac13 thanks! after spending an week I think finally I got a solution. Appreciate your help very much!

1 Like