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:
- You cannot use
update_attribute
in abefore_save
callback or else you will trigger a stack level too deep error by recursively saving the record. - If you use the
before_save
version forafter_touch
nothing will be persisted to the database as save is not implicitly calledafter_touch
.
Aftermath
Our client called and told us how happy she is with the new feature securely in place. Job well done!