Krish <Dev />
ArticlesTutorialsCourses

How to use transaction in MongoDB using Mongoose

So yesterday, while I was working on my project. I had to do some debugging because of the bad data in the database. There are some APIs that update many documents in a few MongoDB collections. Somehow only partial operations succeed.

So I thought if this happens on the production with many users using it, I then will have to spend a lot of time go through the logs, probably might have to write the script or revert the database to fix this kind of issue. I know we enjoy writing code, we don't want to waste time investigating this kind of bug on the production. (You can imagine the pressure on you) So let's fix it.

Problem

mongodb_transaction

From this popular use case, we have the API to transfer the money from your account to someone else account. As you can see, two operators will happen to your database. One is to decrease the money in your account. (Change in one document) And another is to update the payee account. (Another change in another document). Imagine if the first operation succeeds, but somehow, the second one fails. You will then lose the money.

MongoDB Transaction

What? I thought the transaction feature only exists in SQL DB. Yes, that used to be the case, but since MongoDB version 4, now we have it. (Actually, that's a long time ago)

What is MongoDB Transaction?

The same as SQL Database, transaction makes sure that all of the operations will succeed or none of them will. And that's it.

Example (TypeScript)

@Post('money/transfer')
async transferMoney(@Body() dto: TransferMoneyDto) {
  // Start session using mongo connection
  const session = await this.mongoConnection.startSession();
  // Start transaction using the session
  session.startTransaction();

  try {
    // This method will do a few mongo db operations
    const result = await this.transferMoneyService.transfer(dto, session);

    // If transferMoneyService.transfer is resolved, we then commit the transaction
    // This will do the actual writes/updates to the database
    await session.commitTransaction();

    return result;
  } catch (e) {
    // If the error is thrown, we will abort the transaction, so nothing will be changed in the database
    await session.abortTransaction();
    throw e
  } finally {
    // Make sure to end transaction session.
    session.endSession();
  }
}

// The method that will update 2 Mongo documents
async transfer(dto: TransferMoneyDto, session: ClientSession) {
  const { fromAccountId, transferAmount, toAccountId } = dto;
  // Make sure to put session in the query options
  await this.accountModel.findByIdAndUpdate(fromAccountId, { $inc: { amount: -1 * transferAmount } }, { session }).exec()
  await this.accountModel.findByIdAndUpdate(toAccountId, { $inc: { amount: +1 * transferAmount }}, { session }).exec()
}

Notes:

1. In this case, I am using NestJs; the connection can be injected from

constructor(
  @InjectConnection() private readonly mongoConnection: Connection
) {}

2. If you are using mongoose, you can get the connection from

import * as mongoose from 'mongoose';
mongoose.connection

3. During the transaction session, you can not get an updated/created document from the database; you have to commit the transaction first.

4. for model.create operator, even you want to create one document, you have to pass the first argument as an array.

model.create(create, { session }) // does not work
model.create([create], { session }) // use this one

References:


Happy coding. Thanks for reading. Stay safe guys :)

Krish <Dev /> © 2020