Active Record Transactions - Rails Tricks Issue 10
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)
...
endThe 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!