Michael Anhari

Wrapping business logic in ActiveRecord transactions

A top down view of a floppy disk

At some point in your career, it's very likely that you'll be asked to create more than one record in a single request. For features like this, it will make sense to rollback the database changes if anything fails to persist to the database. An all-or-nothing approach. Our answer to this problem lies in the database.

Database transactions

Databases provide the tools we need for this type of feature in the form of transactions. Postgres describes transactions in the following way:

Transactions are a fundamental concept of all database systems. The essential point of a transaction is that it bundles multiple steps into a single, all-or-nothing operation. The intermediate states between the steps are not visible to other concurrent transactions, and if some failure occurs that prevents the transaction from completing, then none of the steps affect the database at all.

In PostgreSQL, a transaction is set up by surrounding your commands with the BEGIN and COMMIT commands.

BEGIN;
UPDATE accounts SET balance = balance - 100.00
    WHERE name = 'Alice';
UPDATE branches SET balance = balance - 100.00
    WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Alice');
UPDATE accounts SET balance = balance + 100.00
    WHERE name = 'Bob';
UPDATE branches SET balance = balance + 100.00
    WHERE account_id = (SELECT id FROM accounts WHERE name = 'Bob');
COMMIT;

Using transactions in ActiveRecord

In ActiveRecord the transaction might look something like this:

alice = Account.find_by(name: "Alice")
bob = Account.find_by(name: "Bob")
ActiveRecord::Base.transaction: do
  alice.update!(balance: alice.balance - 100)
  bob.update!(balance: bob.balance + 100)
  bob.branch.update!(balance: bob.branch.balance + 100)
end

Bangarang!

A photo of the character Rufio from the 1991 film "Hook", who's catchphrase was "Bangarang!"

Notice that we used update! instead of update. In Ruby, methods that end in ! are commonly referred to as "bang methods" and they usually carry the connotation that they will either: mutate data and persist it or raise an exception if they fail.

In this case, the bang method is necessary to raise an exception and trigger the rollback if the update fails (Non-bang versions, i.e. update in this case, will simply add values to the errors attribute on your ActiveRecord object and fail to roll anything back).

Rescuing the failure

If you need to perform some action on the failure condition like displaying info to your user, then you can rescue the exception like any other Ruby exception.

class TransfersController < ApplicationController
  def create
    alice = Account.find_by(name: "Alice") # Hard-coding these, but you get the idea
    bob = Account.find_by(name: "Bob")
    ActiveRecord::Base.transaction: do
      alice.update!(balance: alice.balance - 100)
      bob.update!(balance: bob.balance + 100)
      bob.branch.update!(balance: bob.branch.balance + 100)
    end
  rescue
    flash[:error] = "Your transfer could not be completed."
    redirect_to account_path(alice)
  end
end

Newsletter

I'm working on sending out a weekly newsletter. I'll make it as easy as possible to unsubscribe at any time.