Introduction to Rails transactions

Transactions are an essential part of the Rails framework. Even if you are not using them directly, Rails is doing it automatically each time you save or destroy the instance of the model. However, while transactions can bring you many benefits, they can also damage your application when used in a not proper way. This article introduces transactions and will help you use them wisely, even if you haven’t used them before.

Wait, but do I need to use transactions in my application? The answer is: it depends. If you do many SQL queries that rely on each other in your application, then the knowledge of transactions is a must-have. Otherwise, it is always good to know how things are working before you need them.

What transaction is?

You are part of many transactions every day in real life. The simplest example is the situation where you pay for products in the shop. Such a transaction is successful when you get the product and you pay for it. In a code, such situation can look like the following:

customer = Customer.new("John Doe")
shop = Shop.new("Apple Store")
order = shop.orders.new

order.add(item: "MacBook", quanity: 1)
order.add(item: "Magic Mouse", quantity: 1)

order.hand_over(to: customer) if customer.pay(order)

The transaction is a process where there are two or more steps, and all steps have to be successfully performed to consider the transaction as successfully ended. What’s more, we have to revert all steps in a transaction if one of them can’t be performed successfully. We don’t want to pay for products that we can’t buy, and the shop doesn’t want to give us products for which we didn’t pay. While in real life is easy to control, on the code level, it’s not, and that’s why we have to use database transactions.

The buying process wrapped into a transaction

In the previous example, we have a nice case of the transaction, so we have to make a few changes to handle it properly:

customer = Customer.new("John Doe")
shop = Shop.new("Apple Store")
order = shop.orders.new

order.add(item: "MacBook", quanity: 1)
order.add(item: "Magic Mouse", quantity: 1

ActiveRecord::Base.transaction do
  outcome = customer.pay(order) && order.hand_over(to: customer)
  raise ActiveRecord::Rollback unless outcome
end

Besides the code that I presented in the previous example, there are two new concepts introduced:

  • ActiveRecord::Base.transaction
  • raise ActiveRecord::Rollback

These two elements are the base elements for every transaction: the transaction block and the error that will roll back the transaction if something goes wrong.

How to create a transaction?

Let’s focus on the first base element of the transaction: the transaction block. To create a transaction, you have to wrap all SQL calls of the transaction into the block. All of the following calls are equivalent, and it depends on the context which version you would prefer:

  • ActiveRecord::Base.transaction
  • Model.transaction
  • Model.new.transaction

Since the transaction is part of the ActiveRecord, any model class or model instance provides this method at our disposal.

User.transaction do
  # sql query
  # another sql query
end

Now you know how to create a transaction. I mentioned earlier that transaction is successful when all calls inside the transaction are successful; otherwise, they should be reverted. The process of reverting transactions is as easy as transaction creation.

How to roll back transactions?

Rollback is the name of the process of reverting changes that were made by you or the system. Like the migrations, you can also revert transactions which means that you can revert changes you made to the database inside the transaction.

The rule for reverting transactions is simple: if an error is raised inside the transaction, the transaction is rollbacked; simple as that:

User.transaction do
  user = User.create!(user_attributes)
  user.memberships.create!(membership_attributes)
end

In the above case, I intentionally used create! with the exclamation mark as in case of the failure, the ActiveRecord::RecordNotSaved error would be raised, and the transaction would be automatically rollbacked. On the other hand, if we would use the create method and the creation wouldn’t be successful, the transaction won’t be rollbacked because no error would be raised.

Of course, not always we have the method version with the exclamation mark that raises an error when something goes wrong; that’s why there is a ActiveRecord::Rollback error that we can raise manually to rollback the transaction:

User.transaction do
  user = User.create!(user_attributes)
  outcome = user.perform_some_action
  raise ActiveRecord::Rollback unless outcome
end

What's important if the ActiveRecord::Rollback error is raised the raise is silent so the transaction is rollbacked but error is not raised outside of the transaction. If you want to rollback the transaction and raise the error outside the transaction block, you have to raise a different error.

Catching failed transaction

You can treat transaction as any other block:

begin
  User.transaction do
    user = User.create!(user_attributes)
    outcome = user.perform_some_action
    raise SomeRelatedError unless outcome
  end
rescue SomeRelatedError => e
  # notify user maybe
end

When using rescue with transactions, you should remember that you should not catch all errors, only those you expected to be raised. You don’t want to silence other errors as this can seriously affect your application’s behavior. The below code is invalid:

begin
  User.transaction do
    # do the transaction
  end
rescue StandardError => e
  # catch error
end 

The above code will catch all errors, not only those related to the transaction. Another common mistake when catching errors from transactions is to rescue from ActiveRecord::StatementInvalid - this error indicates that something went wrong on the database level, and we should never silence it.

What are the next steps?

Now you know how to properly use the transaction in a Rails application and prepare the code in case of errors that may be raised when something goes wrong. It would be good to understand how Rails uses transactions under the hood, how the transaction really works, or how to write very advanced transactions. I will cover all of those topics very soon. If you don’t want to miss them, subscribe, and you will be notified about the new publications.