Rails transactions: The Complete Guide

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.

Basic usage

1
2
3
4
5
6
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.

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
def transfer_money
  ActiveRecord::Base.transaction do
    john.update!(money: john.money + 100)
    ted.update!(money: ted.money - 100)
    raise ActiveRecord::Rollback if john.account_is_blocked?
  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 raise an error, it is just used to trigger the rollback from the inside of a transaction.

Nested transactions

But what happens when we have this?

1
2
3
4
5
6
7
8
9
10
11
12
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.

Avoiding Deadlocks

Everything was nice until now. But there is one catch: deadlocks.

When you execute code inside of a transaction block you are keeping your database connection open and locking every row that is affected inside a transaction. Let’s have a look at the code below:

1
2
3
4
5
def johnify!
  ActiveRecord::Base.transaction do
    User.find_each{ |user| user.update!(name: "John") }
  end
end

This code would start locking every single user in your database until the whole transaction finishes.

That is a very probable cause for deadlocks in your system.

For more information about deadlocks in Rails, I recommend this amazing post from Brightbox. It’s a little old but most (if not all of it) still holds true today.

Bonus tips!

Different ways to call a transaction

Along the guide we only showed examples of rails transactions using:

1
2
3
ActiveRecord::Base.transaction do
  # (...)
end

But the same thing could be archived by:

1
2
3
User.transaction do
  # (...)
end

Or even:

1
2
3
4
john.transaction do
  # (...)
end

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 :)

That’s all folks.

Victor A.M.

Victor A.M.
Yet another passionate web developer, mostly experienced with Ruby on Rails and Vue.js.
Currently living in São Paulo and working at Plataformatec.

Vue.js Advanced: Mastering Events

In this post we will take a deep dive into the Vue.js event system, talking about the good practices, helpful tricks and common pitfalls that you will can face. Continue reading

Ruby object interfaces: Public x Private

Published on April 24, 2016

Rails 5: Meet the Active Record OR Query

Published on April 10, 2016