Active Record Transactions - Rails Tricks Issue 10

12 Jun 2023

This week we will look into database transactions. First of all, let me try to explain what they are. A database transaction is a unit of work that encapsulates dependencies and is executed either completely or rolled back to the initial state. For instance, there is double-entry accounting, where you always have a credit and debit record for a transaction, so your accounts stay in balance, and you never want to end up in a situation where you record only one side of a transaction.

If you were working on an app that helps to split the Bill between Users, when someone pays what they are due, you will decrease their balance by that amount and allocate it to the bill:

bill.record_payment_of(amount)
user.decrease_balance_by(amount)

If there is an exception at the second step of the process, the numbers will be unbalanced, and the user will have a bigger balance left than they should’ve. To prevent this from happening, we can wrap this unit into a transaction, and if an exception is raised inside the block, the whole transaction is rolled back:

ActiveRecord::Base.transaction do
  bill.record_payment_of(amount)
  user.decrease_balance_by(amount)
end

In the above example, we would want to force the rollback of the transaction if the user’s balance doesn’t cover the amount needed. To achieve this, we can raise an ActiveRecord::Rollback exception:

# app/models/user.rb

def has_balance_for?(amount)
  ...
end

def decrease_balance_by(amount)
  raise ActiveRecord::Rollback unless has_balance_for?(amount)
  ...
end

The above code will roll back the transaction by raising the exception if there is no balance to cover the necessary amount. You might see that some people start the transaction by calling the transaction method on an actual model class instead of ActiveRecord::Base:

Bill.transaction do
  bill.record_payment_of(amount)
  user.decrease_balance_by(amount)
end

Since your Active Record models are inheriting from ActiveRecord::Base, this is the same as calling transaction directly on the base class. Choosing one over the other is just a matter of preference.

It is worth noting that Active Record uses transactions internally for many operations to maintain data integrity. For instance, when you have a has_many relation, and you have dependent: :destroy set, Active Record will wrap this into a transaction, and if anything happens while destroying the dependent records, the transaction will be rolled back, and the data will stay in the original state.

That’s it for now, until next time!

Hire me for a penetration test

Let's find the security holes before the bad guys do.

Did you enjoy reading this? Sign up to the Rails Tricks newsletter for more content like this!

Or follow me on Twitter

Related posts