Is there anything more satisfying than writing concise, perfect line of SQL and then watching it query your database exactly as you expected? Probably not. Writing — and subsequently watching! — an efficient database query is one of my favorite parts of building an application.
But if there’s one thing about software development that I’ve learned over the past few months, it’s this: projects can get out of control, rather quickly. You start off with your basic models, but then as you add one feature after another, things can start to get out of hand. At that point, your focus as a developer shifts and spreads out to various things. You can’t just care about how readable your code is; you also have to consider how efficient it is, and how different units of your application might be breaking other parts (hopefully not unbeknownst to you!). In other words, you have to consider how defensive and safe your code actually is.
Recently, while writing some background jobs and creating some service objects for a new feature, I realized the importance of executing and enforcing safe code. The good news is that there’s a really helpful, life-saving ActiveRecord transaction method that allows you to do exactly this. And the really great news? You get to watch your SQL queries execute safely as a result!
As our application grows, there are inevitably going to be different models that depend upon each other. To make things more complicated, those models are going to have to change as our Users take different actions, and that means that we’ll need to update different associated parts of our schema as a result. For example, let’s say that we have a background job that’s responsible for processing an
Order on an
User. This background job has a service class that takes care of saving an
Order when it has been charged, billing the
User, and adding a sale to an
Vendor object, which will then be visible on the vendor’s dashboard panel/admin page.
1 2 3 4
In other words, we’re basically transferring money from our
User object to our
Vendor objects. At first glance, this might not seem like a potentially “unsafe” set of queries, but here’s where things can get a bit tricky: imagine that our
User’s credit card information is rejected, or for some reason, the charge cannot be created. In that scenario, we’d want to handle the errors and make sure that the
add_sale method is not called on our
Vendor object. However…we aren’t really safeguarding our code against this situation at all, are we?
But, fear not - it’s not a totally hopeless situation! In fact, we can take care of this problem pretty easily by using ActiveRecord’s
transaction method is defined in the Rails source code under the
ActiveRecord::Transactions module. This method takes a block, and whenever it is invoked, the block that is passed to it will be executed inside of a database transaction. If, in the course of executing that block, an
exception is raised, the database transaction will automatically be rolled back. No SQL will be executed, and no new data will be added to the database
1 2 3 4 5
So what does this mean, exactly? Well, now that we’ve wrapped the logic of our three methods inside of a
transaction method, we can safely assert that in the case that any of these three methods raises an
exception (in other words, fails for any reason), the entire process should fail.
The important piece of this is that we’ll never be adding or updating any data (or writing to our database) unless all of these methods are successful. It’s crucial for us to ensure that this is the case because we’d never want to call the
add_sale method and write data to our
vendor if the
order didn’t successfully
process, or if our
user wasn’t successfully
I like to think of these blocks as a “handshake” between your application and your database: your application and database have an understanding that one will hand off information to the other in a “transaction”, and when the deal actually goes through, they shake hands and make it official…or something more poetic. If that made no sense — or if you’re not a big fan of metaphors — here’s how the Rails documentation explains
“Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs. So basically you should use transaction blocks whenever you have a number of statements that must be executed together or not at all.”
The Rules of Transacting
Because ActiveRecord transactions can be easily explained and simplified as a single method, it’s easy to forget the idiosyncrasies that make this method work. There are a few things to keep in mind so that we can get the most out of these blocks.
1. Opening database connections
A transaction opens up a single database connection. This means that when we call the
transaction method, the method can only be invoked on the current database connection. This is important to remember if our application writes to multiple database at once; for example, if our
Order and our
Vendor data lived in two different databases, we’d need to nest our transactions:
1 2 3 4 5 6 7
It’s generally a good idea to avoid nested transactions, mostly because the relationship between parent and child transactions can get complicated. This is especially the case because rollbacks are contained inside of their transactions blocks. I think that Mark Daggett explains this pretty well in his blog:
“ActiveRecord::Rollback does not propagate outside of the containing transaction block and so the parent transaction does not receive the exception nested inside the child. I find it easier to think of nested transactions like the child who dumps its contents into the parent container, leaving the child transaction empty. To ensure a rollback is received by the parent transaction you must add the
requires_new: true. option to the child transaction.”
2. Different classes, one transaction
Because transactions are bound to database connections, we can mix different types of models inside of a
transaction block. In fact, that’s exactly what we were doing when we wrote our initial transaction:
1 2 3 4 5
3. Class and instance methods
The great part about
transaction is that it is available to us as both a class and an instance method for our ActiveRecord models. What does this mean, exactly? Well, the short answer is that we can write a transaction is lots of different ways, since we can invoke the
transaction method on a class or an instance.
For example, we could have written this:
1 2 3
1 2 3
Or any of these:
1 2 3 4 5 6 7 8
And if we were writing a method inside of the
User classes, these options would have worked as well:
1 2 3 4 5
The key here is that the
transaction can be called on any class that inherits from
ActiveRecord::Base. Why is that the key? Well, you might remember that we initially started off wanting to write a transaction inside of our service object…right? In that case, we can’t use something like
transaction do, because
self is the service object class, which does not inherit from
So, what do? Well, just call the transaction method onto
ActiveRecord::Base directly! there’s a quick fix for that.
1 2 3
When in doubt, we can always just call the
transaction method onto the
ActiveRecord::Base class directly to be sure that it will run.
4. Exceptions are the rule
There’s one golden rule of the
transaction block: it will only rollback the transaction if an error is raised.
Why is this important? Well, calling something like
destroy inside of a transaction will not raise an error; if something goes wrong, these methods will simply return
false. Which means that our
transaction block will continue, since there was no error raised!
Uh oh…how to fix? Just use the
destroy! methods instead! These are both ActiveRecord methods which raise an exception if they don’t execute successfully:
1 2 3 4
And that’s exactly what we need in this case, because we want the entire transaction to be closed if one or both of these methods are unsuccessful, and we want to tell the database that no data has changed. If we really, really wanted to use
save instead of
save!, we’d have to manually
raise an error in the block for our transaction to work as expected.
Transactions Under The Hood
One of the interesting things about how
transaction works under the hood has to do with how the
destroy methods work. It turns out that Rails actually wraps the
destroy methods in their own
transactions! So, we were using
ActiveRecord::Base.transaction all along, without probably ever knowing what was really happening! Pretty crazy, right?
There are a couple reasons that
destroy are particularly curious. First, because they each occur in their own transactions, this means that we’ll never need to write a
transaction block to update a single record. In fact, that’s exactly what Rails is doing for us when we call something like
@user.save — it’s running a
transaction block behind the scenes.
The second reason this is interesting is because of callbacks that are associated with these two methods. A callback hook like
after_save is actually part of the same active transaction that was opened when we called
@user.save. So, if we wanted our code to execute outside of Rails’ default
transaction that wraps around
destroy, we’d want to use callback hooks like
after_destroy. If we want something specific to happen when the
transaction succeeds, we’d have to use the
after_commit callback, and if we want something specific to happen when the
transaction fails, we could use the
While reading about transactions and the
destroy methods, I discovered another really interesting method called
with_transaction_returning_status. The transactions source code is a bit of a rabbit hole, but if we spend some time reading through it, there’s a lot to learn.
In a nutshell, the
with_transaction_returning_status is responsible for actually raising an
ActiveRecord::Rollback. Somewhere inside of the black box of Rails magic, when we see an error caused by a
save transaction that looks like this:
1 2 3 4 5 6
it’s actually a method like
with_transaction_returning_status that’s responsible for causing that rollback to happen! Here’s a truncated example of the source code; we can see that this method defines a local
status variable, sets and returns the
status if the
transaction is successful, and
raises an error, if the
status is not defined:
1 2 3 4 5 6 7 8 9 10 11
The Rails documentation also briefly explains exactly what’s happening:
methodwithin a transaction and captures its return value as a status flag. If the status is true the transaction is committed, otherwise a ROLLBACK is issued. In any case the status flag is returned.
Transactions are all around us, it seems, from the most everyday methods to the more complicated ones that we write ourselves. They’re a great way of keeping our code safe while also defending against things like duplicate SQL queries.
transactionmethod takes a block, and will only execute the block and write to your database if no exceptions are raised.
- You can defined the
transactionmethod on any class that inherits from
ActiveRecord::Base, and that transaction will open up a single new database connection.
- Check out the Jeff Casimir helpful summary of transactions in his Gist. Or, if you’re looking for a challenge, head over to the Rails documentation on ActiveRecord transactions.