I use a database transaction at some point in virtually every Rails project I tackle. If you aren’t aware, a transaction forces a group of queries to succeed or fail together. If any ActiveRecord object in the series throws an exception, all changes are rolled back for all records involved (documentation). The classic example is debit-credit accounting, which always inserts two records for every transaction.

ActiveRecord::Base.transaction do
  debit.save!
  credit.save!
end

Note the bang methods. A regular validation error will not trigger a rollback. We have to elevate validation errors to exceptions, which means we’ll need to rescue those exceptions. Plus, we need to track the success of the operation and report back to the user.

begin
  ActiveRecord::Base.transaction do
    debit.save!
    credit.save!
  end
  success = true
rescue ActiveRecord::RecordInvalid
  success = false
end

That’s a lot of boilerplate, but we can DRY it out.

class ApplicationController < ActionController::Base
  private
    def with_transaction
      ActiveRecord::Base.transaction { yield }
      true
    rescue ActiveRecord::RecordInvalid
      false
    end
# some update action
success = with_transaction do
  debit.save!
  credit.save!
end

A better transactional save

To use save!, as in the previous examples, attributes must be set in a separate, preceeding step. In practice, a simple update action may use a params method to update a record in one step, plus check the return value to redirect or render errors. Our hypothetical accounting application might have an accounts controller, for example.

class AccountsController < ActionController::Base
  ...
  def update
    if @account.update(account_params)
      redirect_to accounts_url
    else
      render :edit
    end
  end

Even with our new function, a transactional update is pretty verbose.

class TransfersController < ActionController::Base
  ...
  def update
    @debit.amount = params[:amount]
    @credit.amount = params[:amount]

    if @debit.valid? && @credit.valid?
      success = with_transaction do
        @debit.save!
        @credit.save!
      end
    else
      success = false
    end

    if success
      redirect_to transfers_url
    else
      render :edit
    end
  end

Notice that manual validation check before the transaction? Without it, an earlier valid record can trigger a query that will be rolled back by a later invalid record. If we can determine that a record is invalid at the application level, we probably want to avoid hitting the database at all. Let’s extract another function.

class ApplicationController < ActionController::Base
  private
    ...
    def transactional_save(objects)
      if objects.all?(&:valid?)
        with_transaction do
          objects.each { |o| o.save! }
        end
      else
        false
      end
    end
class TransfersController < ActionController::Base
  ...
  def update
    @debit.amount = params[:amount]
    @credit.amount = params[:amount]

    if transactional_save([@debit, @credit])
      redirect_to transfers_url
    else
      render :edit
    end
  end

We still have the extra step to set attributes, but I can live with that.

What goes up…

Transactionally destroying multiple objects is a slightly different problem. We don’t need validation, but we do need to catch a different exception. We can modify with_transaction slightly to accomodate this and then write one more simple function. Here’s the final version of the code:

class ApplicationController < ActionController::Base
  private
    def with_transaction(e=ActiveRecord::RecordInvalid)
      ActiveRecord::Base.transaction { yield }
      true
    rescue e
      false
    end

    def transactional_save(objects)
      if objects.all?(&:valid?)
        with_transaction do
          objects.each { |o| o.save! }
        end
      else
        false
      end
    end

    def transactional_destroy(objects)
      with_transaction(ActiveRecord::RecordNotDestroyed) do
        objects.each { |o| o.destroy! }
      end
    end
end

Because we have no attributes to set, a transactional destroy action is nearly as compact as any other destroy.

class TransfersController < ActionController::Base
  ...
  def destroy
    if transactional_destroy([@debit, @credit])
      redirect_to transfers_url
    else
      render :edit
    end
  end

These simple convenience functions are not intended to be a universal solution to transactional operations, but they address most of my needs. As always, if you think I’m doing it wrong, please feel free to tell me.