Rails transactions: The Complete Guide
Rails transactions are a way to ensure that a set of database operations will only occur if all of them succeed. Otherwise they will rollback to the previous state of data.
Our examples will demonstrate it in the most useful scenario for transactions: money transfers. In this case you only want Ted to receive money if John loses the same money.
def transfer_money ActiveRecord::Base.transaction do john.update!(money: john.money + 100) ted.update!(money: ted.money - 100) end end
Simple, isn’t it?
This way if any error happens inside the transaction block the entire operations will be roll-backed to the previous state.
This is why we use the .update! instead of just .update. Since a normal update doesn’t raise errors and just return “false”, it would not trigger a rollback in the transaction.
Remember that the errors thrown by the .update! will still show an error page for you user. But we will learn how to treat that in the next topic.
Rescuing from Transaction Errors
Since we are using the ! methods from ActiveRecord, we should expect some errors to occur. The most common of them is the ActiveRecord::RecordInvalid.
This error is thrown when for some reason the changes you want to make in the records would turn them invalid.
Other errors may occur in your rails transactions and it is up to you to decide which ones you will treat.
def transfer_money(amount) ActiveRecord::Base.transaction do john.update!(money: john.money + amount) ted.update!(money: ted.money - amount) end rescue ActiveRecord::RecordInvalid puts "Oops. We tried to do an invalid operation!" end
In this example the “amount” argument could be invalid, leading to an invalid value for the “money” attribute.
We take care of this rescuing from the RecordInvalid error and printing out a friendly message to our users.
Triggering a Rollback
Sometimes we want to cancel the transaction manually. Like in the following example:
def transfer_money ActiveRecord::Base.transaction do john.update!(money: john.money + 100) ted.update!(money: ted.money - 100) raise ActiveRecord::Rollback if john.is_an_asshole? end end
In this scenario we raise an error according to the business logic of our application.
One thing to take note is that the ActiveRecord::Rollback error does not actually raises an error, it is just used to trigger the rollback from the inside of a transaction.
But what happens when we have this?
def transfer_money ActiveRecord::Base.transaction do john.update!(money: john.money + 100) ted.update!(money: ted.money - 100) ActiveRecord::Base.transaction do transfer.create!(amount: 100, receiver: john, sender: ted) end end end
In this case each transaction will happen and rollback independently of one another. But will still be tied to the same database connection (see the bonus tips for a little more insight on this).
A thing to notice is that in the example above an error inside the inner transaction WILL rollback the outer transaction, because we are not rescuing anything.
Different ways to call a transaction
Along the guide we only showed examples of rails transactions using:
But the same thing could be archived by:
There is actually no difference between any of those, but I personally try to use record.transaction as much as I can, because I find it easier to test the transaction later (for example, making a mocked record that responds to the method .transaction with an error).
Rails transactions are tied to one database connection
And as long as the transaction block is running this one database connection is open. So try to do as little as needed inside the transaction block, otherwise you will be blocking a database connection for more time than you should.
Know something that is not in the guide? Share with us in the comments