Getting Touchy With ActiveRecord Callbacks

What?

When used correctly ActiveRecord::Callbacks provide a great way to trigger logic before and after the life cycle of your objects. One of my favorite callbacks is touch.

As those of you who are familiar with Unix conventions are already aware, touch is used to update a file's timestamp to the present moment. In Rails, calling touch on an ActiveRecord object triggers a modification on the updated_at column with the current time.

Background Information

Let's say we have an instance of a Receipt and we call receipt.touch on it. The result is ActiveRecord executing a SQL statement:

UPDATE `receipts` SET `updated_at` = 'current_date_time' WHERE `receipts`.`id` = 1;

Okay, this is somewhat interesting, but why would we want to update that column without making any other modifications?

Enter the Client

Your client has called and is requesting a new feature. Her users have been adding items to their receipts, but they have been emailing her company to let someone know that the system really should be able to automatically tally up the various amounts for each item and persist that to the parent receipt itself.

This might sound like a fairly complex request; however, there is a relatively simple solution for this problem.

Touching (ActiveRecord) Base

In app/models/item.rb:

class Item < ActiveRecord::Base

  belongs_to :receipt, touch: true

  validates_presence_of :price
  validates_numericality_of :price

end

Here, we specify that items belong to a receipt and if an item is modified it should touch the parent receipt. We also guarantee that each item has a price and that the price is in fact numerical.

In app/models/receipt.rb:

class Receipt < ActiveRecord::Base

  has_many :items

  accepts_nested_attributes_for :items, :allow_destroy => true

  validates_presence_of :taxes
  validates_numericality_of :taxes 
  validates_length_of :items, :minimum => 1

  before_save :recalculate_taxes
  after_touch :recalculate_items

  protected

  def recalculate_taxes
    self.amount = amount_total
  end

  def recalculate_items
    update_attribute(:amount, amount_total)
  end

  private

  def amount_total
    items.sum(:price) + self.taxes
  end

end

Here, we see that receipts require a numerical value for taxes and at least one item in order to be considered valid. We also see the presence of two ActiveRecord::Callbacks, namely, before_save and after_touch.

before_save

When the receipt itself is saved the value for taxes may have been altered, so we need to modify it's value in place before the call to save submits the changes to the database.

after_touch

In this case, when a receipt is touched by an item, the receipt needs to recalculate it's amount attribute and update it without triggering further ActiveRecord::Callbacks. This is where update_attribute comes in, performing a direct SQL UPDATE on the specified column with the specified value. Please be aware, update_attribute can be extremely dangerous because it does not trigger validations.

In this example, we have ensured that the parent and child attributes involved in the evaluation are already numerical which has the effect of validating the function's inputs by proxy prior to submitting them for evaluation.

Why two versions of the same method?

There are two reasons:

  1. You cannot use update_attribute in a before_save callback or else you will trigger a stack level too deep error by recursively saving the record.
  2. If you use the before_save version for after_touch nothing will be persisted to the database as save is not implicitly called after_touch.

Aftermath

Our client called and told us how happy she is with the new feature securely in place. Job well done!