Archive for June, 2009

Why You Don’t Need an AfterFind Callback in Rails

June 4th, 2009

I came to Rails from a background in CakePHP. In the course of my experience with that framework, I really started to embrace the concept of Model callbacks: methods that are run automatically at specifics points during common actions, like beforeSave, beforeRender, afterSave, and afterFind, to name a few.

I used to use the afterFind callback quite a bit in Cake, mainly to standardize the data coming out of the model, or to run occasional queries that I didn’t want to associate directly through the find() method.

So when I moved over to Rails, I started to poke around a bit looking for callback methods. And while I found them, there seemed to be a lack of focus on the after_find callback, as well as a general lack of details or examples online. Plus, it looked like there’s a huge performance hit when you use it.

So why the lack of focus? Surely most people want ways to play with returned data from a find() method call?

And then I realized what is probably obvious to most people: Rails returns model objects, not arrays.

Objects have that nice feature of being able to define arbitrary methods that you can execute at will. This ability limits the need for an after_find method to go through and add or change some data in most cases. This is why it seems to be rarely used.

To really see the difference, let’s look at a slightly contrived example.

Say I have a Product model that includes a field for weight (in pounds). When I display this data in my view, I want to display the English units and the metric units for its weight, but I don’t want to write out the conversion code in the view every time.

Using the afterFind() callback in CakePHP, I’d have to do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
class Product extends AppModel {
 
  private function add_weight_types($data) {
    if (!empty($data) && is_array($data)) {
      if (isset($data[$this->alias]) && isset($data[$this->alias]['weight']) {
        $data[$this->alias]['weight_in_lbs'] = $data[$this->alias]['weight']. ' lbs';
        $data[$this->alias]['weight_in_kg'] = ($data[$this->alias]['weight'] / 2.2) . ' kg';
      }
   }
   return $data;
  }
 
  function afterFind($data) {
    if (!empty($data)) {
      if (isset($data[$this->alias])) {
        return $this->add_weight_types($data);
      } elseif {
        for ($i=0;$i<count($data);$i++) {
          $data[$i] = $this->add_weight_types($data[$i]);
        }
      }
    }
    return $data;
  }
 
}
?>

Notice I had to take into account the two differing data structures that CakePHP returns: the find(’all’) method that returns an array of model arrays, or the find(’first’) method that returns a single model array. Every find call now adds two additional array items for the weight that I can use in my views:

echo 'Product Weight: '.$data['Product']['weight_in_lbs']. ' ('. $data['Product']['weight_in_kg'] . ')';

Rails, because it returns arrays of model objects, allows us to have a much simpler implementation. Start in the Product Model again:

1
2
3
4
5
6
7
8
9
10
11
class Product < ActiveRecord::Base
 
  def weight_in_kg
    "$.2f kg" % (weight.to_f / 2.2)
  end
 
  def weight_in_lbs
    "$.2f lbs" % weight
  end
 
end

Notice there’s no callback needed for this. The code only executes when it’s called in a view, like this:

Product Weight: <%= @product.weight_in_lbs %> (<%= @product.weight_in_kg %>)

Man, that’s nice. It just feels good, you know?

CakePHP is great in many, many ways, but for someone who learned a lot about programming by using that framework, I’m realizing now how many unconscious thought patterns it instilled in me. Rails is a good extension of that framework concept at a higher level of complexity, and after working with Rails for a while, I began to break out of the Cake thought-pattern (and most likely moved well in the Rails thought-pattern instead).