Multi-Document ACID Transactions in MongoDB with Go
Rate this quickstart
The past few months have been an adventure when it comes to getting started with MongoDB using the Go programming language (Golang). We've explored everything from create, retrieve, update, and delete (CRUD) operations, to data modeling, and to change streams. To bring this series to a solid finish, we're going to take a look at a popular requirement that a lot of organizations need, and that requirement is transactions.
So why would you want transactions?
There are some situations where you might need atomicity of reads and writes to multiple documents within a single collection or multiple collections. This isn't always a necessity, but in some cases, it might be.
Take the following for example.
Let's say you want to create documents in one collection that depend on documents in another collection existing. Or let's say you have schema validation rules in place on your collection. In the scenario that you're trying to create documents and the related document doesn't exist or your schema validation rules fail, you don't want the operation to proceed. Instead, you'd probably want to roll back to before it happened.
There are other reasons that you might use transactions, but you can use your imagination for those.
In this tutorial, we're going to look at what it takes to use transactions with Golang and MongoDB. Our example will rely more on schema validation rules passing, but it isn't a limitation.
Since we've continued the same theme throughout the series, I think it'd be a good idea to have a refresher on the data model that we'll be using for this example.
In the past few tutorials, we've explored working with potential podcast data in various collections. For example, our Go data model looks something like this:
While we had other collections, we're going to focus strictly on the
episodescollection for this example.
Rather than coming up with complicated code for this example to demonstrate operations that fail or should be rolled back, we're going to go with schema validation to force fail some operations. Let's assume that no episode should be less than two minutes in duration, otherwise it is not valid. Rather than implementing this, we can use features baked into MongoDB.
Take the following schema validation logic:
The above logic would be applied using the MongoDB CLI or with Compass, but we're essentially saying that our schema for the
episodescollection can contain any fields in a document, but the
durationfield must be an integer and it must be at least two. Could our schema validation be more complex? Absolutely, but we're all about simplicity in this example. If you want to learn more about schema validation, check out on the subject.
Now that we know the schema and what will cause a failure, we can start implementing some transaction code that will commit or roll back changes.
Before we dive into starting a session for our operations and committing transactions, let's establish a base point in our project. Let's assume that your project has the following boilerplate MongoDB with Go code:
The collection must exist prior to working with transactions. When using the
RunCommand, if the collection already exists, an error will be returned. For this example, the error is not important to us since we just want the collection to exist, even if that means creating it.
The goal here will be to try to insert a document that complies with our schema validation as well as a document that doesn't so that we have a commit that doesn't happen.
After defining the transaction options, we start a session which will encapsulate everything we want to do with atomicity. After, we start a transaction that we'll use to commit everything in the session.
Inside the session, we are doing two
InsertOneoperations. The first would succeed because it doesn't violate any of our schema validation rules. It will even print out an object id when it's done. However, the second operation will fail because it is less than two minutes. The
CommitTransactionwon't ever succeed because of the error that the second operation created. When the
WithSessionfunction returns the error that we created, the transaction is aborted using the
AbortTransactionfunction. For this reason, neither of the
InsertOneoperations will show up in the database.
Starting and committing transactions from within a logical session isn't the only way to work with ACID transactions using Golang and MongoDB. Instead, we can use what might be thought of as a more convenient transactions API.
Take the following adjustments to our code:
Instead of using
WithSession, we are now using
WithTransaction, which handles starting a transaction, executing some application code, and then committing or aborting the transaction based on the success of that application code. Not only that, but retries can happen for specific errors if certain operations fail.
You just saw how to use transactions with the MongoDB Go driver. While in this example we used schema validation to determine if a commit operation succeeds or fails, you could easily apply your own application logic within the scope of the session.
If you want to catch up on other tutorials in the getting started with Golang series, you can find some below:
Since transactions brings this tutorial series to a close, make sure you keep a lookout for more tutorials that focus on more niche and interesting topics that apply everything that was taught while getting started.
How to Build a Go Web Application with Gin, MongoDB, and with the Help of AI
Sep 27, 2023 | 11 min read