How to Implement Transaction using Mongoose
Technologies: MongoDB Transaction, Mongoose, NodeJS and ExpressJS
Before we dive into the implementation part, let's discuss Transactions together with some examples. This will help us to understand what Transaction is, why it exists and when we need to use it.
What is a transaction in MongoDB?
A transaction in MongoDB is a way to ensure that a set of database operations are performed together as a single unit of work. Transactions help maintain data integrity and consistency when multiple changes need to be made to the database. Think of a transaction as a container that holds a series of actions, like updating multiple documents or collections. Either all the actions within the transaction are completed successfully, or if any of them fail, none of the changes take effect. This helps prevent situations where some changes happen and others don't, leaving the database in an unpredictable state.
Why do we need transactions?
In databases, it's important to ensure that data remains accurate and reliable, especially when multiple changes are happening at the same time. Imagine scenarios like transferring money between bank accounts or updating inventory levels in an online store. Without transactions, if one part of the operation succeeds while another fails, you could end up with incorrect or inconsistent data. Transactions provide a safety net by ensuring that either all changes are applied or none at all. This is crucial for maintaining data integrity and avoiding scenarios where the database becomes unreliable or unusable.
How can transactions improve our application?
Transactions can significantly improve the reliability and quality of your application. They provide a way to handle complex operations involving multiple data changes in a controlled and predictable manner. By ensuring that either all changes are committed or none at all, transactions help maintain data consistency and prevent situations where the database becomes unreliable. This can lead to better user experiences, fewer errors, and increased trust in your application. Whether you're building a financial system, an e-commerce platform, or any application that deals with critical data, using transactions can help you avoid headaches and ensure that your data remains accurate and dependable.
What do you need to know to use Transactions?
Session: MongoDB transactions are managed using a session. A session represents a logical unit of work (Write, Update or Delete query) and acts as a container/bucket for transactions. You'll need to understand how to start and end sessions.
Start a Transaction: You'll need to learn how to begin a transaction within a session. This is typically done using the startTransaction method.
Commit and Abort: Transactions can be committed (save all changes) or aborted (undo all the changes). You should learn about the commitTransaction and abortTransaction methods.
Write Operations: Transactions involve write operations, such as inserts, updates, and deletes. Learn about the various write operation methods available in Mongoose, like create, insertMany, updateOne, updateMany, deleteOne, etc.
Isolation and Consistency: Understand the isolation levels provided by transactions. These levels determine how transactions interact with each other and affect data consistency. Common isolation levels include readCommitted and snapshot.
Read Operations: While transactions are focused on write operations, they also impact read operations. Learn about how read operations within a transaction are affected by uncommitted writes and how to ensure consistent reads.
Error Handling: Transactions can fail for various reasons. Learn how to handle errors during transactions, including detecting errors and handling rollbacks.
Retry Logic: In a distributed system, transactions might face temporary failures. You'll need to understand how to implement retry logic to handle transient issues. The transaction commit and abort operations are retryable write operations. If the commit operation or the abort operation encounters an error, MongoDB drivers retry the operation a single time regardless of whether retryWrites is set to false. The write operations inside the transaction are not individually retryable, regardless of the value of retryWrites.
Nested Transactions: MongoDB supports nested transactions within a single session. Learn how to work with nested transactions and their behavior.
Write Concern: Learn about write concern options that control the acknowledgment of write operations within transactions.
Write Conflict: MongoDB uses optimistic concurrency control to ensure consistency in its transactions. Like, if two or more transactions attempt to modify the same document concurrently, MongoDB will throw a WriteConflict error, indicating that one or more transactions failed due to conflicts.
How to Implement Transactions using Mongoose in ExpressJS application?
In the following example, we are assuming that you already have an up-and-running ExpressJS application. You can run CRUD operations in this project and the project is using the MVC pattern, where all APIs are written in controllers.
Transactions can be implemented using 5 easy steps. These steps are:
First Step: Require Mongoose in your controller, as shown below image:
const mongoose = require('mongoose');
Second Step: In any of the APIs, create a session and start a Transaction using the following syntax:
//Transaction Session
const session = await mongoose.startSession();
session.startTransaction();
Third Step: Pass the session variable in each of the Write, Update and Delete queries within a session as shown in below:
// Query 1: create Query
try {
await grnParentModel.create([{name: 'John Doe'}], {session});
console.log('✅ Step 1/2: Successfully created a new record');
} catch (err) {
console.log("❌ Step 1/2: Failed to create a new record",err);
return res.status(400).json({
success: false,
message: 'Failed to create a new record'
});
}
// Query 2: inserMany Query
try {
await grnParentModel.insertMany(
[
{name: 'John Doe'},
{name: 'Jane Doe'},
],
{session}
);
console.log('✅ Step 1/2: Successfully created 2 new records');
} catch (err) {
console.log("❌ Step 1/2: Failed to create 2 new records",err);
return res.status(400).json({
success: false,
message: 'Failed to create 2 new records'
});
}
// Query 3: deleteOne Query
try {
await grnParentModel.deleteOne({name: 'John Doe'}, {session});
console.log('✅ Step 1/2: Successfully deleted a record');
} catch (err) {
console.log("❌ Step 1/2: Failed to deleted a record",err);
return res.status(400).json({
success: false,
message: 'Failed to deleted a record'
});
}
Please note that the Read query does not require passing any session because the Read query does not change the state of any data.
Fourth Step: Handle the errors. If there is any error in any of the queries, then we should abort the transaction. We can abort transactions in each of the Write, Update and Delete queries within a single session. The purpose of aborting a transaction is to undo all the changes in a single session. The syntax is shown below:
// Query 1: create Query
try {
await grnParentModel.create([{name: 'John Doe'}], {session});
console.log('✅ Step 1/2: Successfully created a new record');
} catch (err) {
console.log("❌ Step 1/2: Failed to create a new record",err);
// Rolling back the changes using session abort
await session.abortTransaction();
session.endSession();
return res.status(400).json({
success: false,
message: 'Failed to create a new record'
});
}
// Query 2: inserMany Query
try {
await grnParentModel.insertMany(
[
{name: 'John Doe'},
{name: 'Jane Doe'},
],
{session}
);
console.log('✅ Step 1/2: Successfully created 2 new records');
} catch (err) {
console.log("❌ Step 1/2: Failed to create 2 new records",err);
// Rolling back the changes using session abort
await session.abortTransaction();
session.endSession();
return res.status(400).json({
success: false,
message: 'Failed to create 2 new records'
});
}
// Query 3: deleteOne Query
try {
await grnParentModel.deleteOne({name: 'John Doe'}, {session});
console.log('✅ Step 1/2: Successfully deleted a record');
} catch (err) {
console.log("❌ Step 1/2: Failed to deleted a record",err);
// Rolling back the changes using session abort
await session.abortTransaction();
session.endSession();
return res.status(400).json({
success: false,
message: 'Failed to deleted a record'
});
}
The Fifth Step is to commit to all the changes. If transactions are not committed, then what will happen is that no changes will be done.
// Committing transaction session
await session.commitTransaction();
session.endSession();
The complete example looks like this:
const mongoose = require('mongoose');
async function transactionExample(){
//Transaction Session
const session = await mongoose.startSession();
session.startTransaction();
// Query 1: create Query
try {
await grnParentModel.create([{name: 'John Doe'}], {session});
console.log('✅ Step 1/2: Successfully created a new record');
} catch (err) {
console.log("❌ Step 1/2: Failed to create a new record",err);
// Rolling back the changes using session abort
await session.abortTransaction();
session.endSession();
return res.status(400).json({
success: false,
message: 'Failed to create a new record'
});
}
// Query 2: inserMany Query
try {
await grnParentModel.insertMany(
[
{name: 'John Doe'},
{name: 'Jane Doe'},
],
{session}
);
console.log('✅ Step 1/2: Successfully created 2 new records');
} catch (err) {
console.log("❌ Step 1/2: Failed to create 2 new records",err);
// Rolling back the changes using session abort
await session.abortTransaction();
session.endSession();
return res.status(400).json({
success: false,
message: 'Failed to create 2 new records'
});
}
// Query 3: deleteOne Query
try {
await grnParentModel.deleteOne({name: 'John Doe'}, {session});
console.log('✅ Step 1/2: Successfully deleted a record');
} catch (err) {
console.log("❌ Step 1/2: Failed to deleted a record",err);
// Rolling back the changes using session abort
await session.abortTransaction();
session.endSession();
return res.status(400).json({
success: false,
message: 'Failed to deleted a record'
});
}
// Committing transaction session
await session.commitTransaction();
session.endSession();
}
For any queries and suggestions, please leave a comment. You can reach me on LinkedIn.