DEV Community

FOLASAYO SAMUEL OLAYEMI
FOLASAYO SAMUEL OLAYEMI

Posted on

How to perform transactions in MongoDB using Node.js

What is transaction?

A transaction is a unit of work made up of several operations that you wish to either succeed together or fail together if one or more of them fail.
Atomity is the term used to describe this behavior.
Atomicity is a property that allows transactions made up of one or more operations to happen all at once without being visible to other clients and without leaving any changes if one of the operations fails.

When you need to make an atomic update that modifies numerous documents, known as a multi-document transaction, you may benefit from transactions the most because all write operations on a single document in MongoDB are atomic.
Multi-document transactions are ACID compliant, similar to write operations on a single document, which means MongoDB ensures the data involved in your transaction activities stays consistent even if it runs into unforeseen failures.

Multi-document transactions in MongoDB take place within a client session.
A set of connected read or write operations that you want to make sure happen in order or sequentially is known as a client session.
The driver can ensure causal consistency between the operations when used in conjunction with the majority read and write concerns.

How to perform transactions in MongoDB using Node.js

In some relational database like SQL or MySQL we have the concept of transaction which basically means a group of operations that should be performed as a unit.
So, either all these operations will complete and change the state of the database or if something fails in the middle, all these operations that have been applied will be rolled back and our database will go back in the initial state.

Now, in MongoDB we don't have transactions as we have in these relation databases, we have a technique called Two Phase Commit. Click on this link to learn more on how to perform two phase commit. This document clearly explain how to perform two phase commit with a real world example.

Note, clone this project from github, open the after folder, then open the rental.js file. This is important for better clarification of the explanations below.

In my own case I'm going to introduce you to a library that gives us the concept of transaction, but internally it implement this transaction using the two phase commit technique. Now, open your terminal to install the library using npm install fawn, If you already clone the github repo, all you need to do is to run npm install from your terminal in your folder directory or root directory:

const Fawn = require("fawn");
Enter fullscreen mode Exit fullscreen mode

You can locate the code above at the top of your rentals.js file to load fawn. Now this is a class, it has an initialize method that we need to call:

Fawn.init(mongoose);
Enter fullscreen mode Exit fullscreen mode

Brief explanation to the code sample above:
We created a fawn initialized method from the fawn class, then passed a mongoose object.

try {
    new Fawn.Task()
      .save("rentals", rental)
      .update(
        "movies",
        { _id: movie._id },
        {
          $inc: { numberInStock: -1 },
        }
      )
      .run();

    res.send(rental);
  } catch (ex) {
    res.status(500).send("Something failed.");
  }
Enter fullscreen mode Exit fullscreen mode

Brief explanation to the code sample above:

Lastly, in the post method of the rentals API we create a task object, which is like a transaction. Then, we add some fawn operations together, which will be treated as a unit. So, we want to save this new rental to the rentals collection. That's why we call save, then pass the name of our collection which is rentals. And as a second argument, we pass our new rental object. Note that, in the code sample above you are working directly with the collection. That's why we need to pass the actual name of the collection, which is plural, not singular, and also note that this name is case sensitive. When you check your MongoDB compass, you can see the name of our collections, they're all lowercase.

Database Collections

Now, as part of the unit, we also want to update the movies collection. That's why we pass a query object to determine the movie, or movies that should be updated, so we set the _id to movie._id.

Finally, the third argument is our update object. So, here we use the $inc, then we set the value of this to an object. The target property we update is numberInStock. Now, we decrement the value of this property by 1, so we pass minus -1. After we chain all these operations, then finally we call run(). If you don't call run(), none of these operations will be performed. It's possible that something failed during this transaction. So, we wrapped the code in the try catch block. The try block is for a success scenario, while the catch block is used to catch an exception which return 500 error to the client. response.status(500) which means internal server error. Then, we pass in a message which will be displayed if something failed in the code.

Now, run the application, then open your MongoDB compass. Look at our movies collection, where we have our movies and number in stock is 8.

number in stock is 8 screenshot

Note that, in the screenshot below, we created a new rental with postman which change the rental's numberInStock to 7. We did this by sending a post request to our rental's endpoint.

number in stock 7 screenshot

In the body request we have a valid customerId and movieId, which give us a response containing our new rental's object.
customerId & movieId screenshot

Postman new response to inputted customerId and movieId screenshot

You will notice that we only set customer and movie in the rental object, then in our try block we create the task, run it, and then send the rental object to the client.
We didn't set the _id or dateOut property, but in the body of the response you can see both _id and dateOut properties are set. So, how did this happen? In MongoDB we don't have these default values. We define them with our mongoose Schema. So when we create a new rental object, mongoose knows the Schema for this object, it looks at various properties and set the default values. The same is true for the id properties, so more accurately MongoDB doesn't set this. This property is set before we save this document to the database.

Lastly, in the MongoDB compass if you check the database, you will notice that in the list of collections, you see this new collection, we did not create these anywhere in our code, so where did that come from?
Fawn auto generated collection screenshot

The Fawn library that we installed uses that collection to perform two phase commits. So when you run this task, it add a new document to that collection, which represents our transaction. Then, it will execute each of these operations independently, and finally, when all these operations are complete, it will delete that document from this collection.

Thanks for reading...
Happy Coding!

Top comments (0)