RailsConf Recap: Named Callbacks
Another topic we touched briefly on at RailsConf was the idea of named callbacks.
Consider this snippet (also from Brian Cooke’s expense tracking application):
1 2 3 4 5 6 7 8 |
class Expense < ActiveRecord::Base protected def before_create if self.created_at == Time.now.to_date.to_time self.created_at = Time.now end end end |
One thing to keep in mind here is that when a new Expense record is created, the created_at column is used to track when the expense originally occurred, not when the record was created. As a special case, if the timestamp is 00:00 of the current day, then it is assumed to actually be the current time.
Now, looking at that code, it’s definitely not immediately obvious what it is trying to do. In fact, it took me a few minutes of steady concentration (and cross-referencing other parts of the project) to understand it. The fact that it uses a generic “before_create” callback makes it hard to know the purpose of the method, and the use of “Time.now.to_date.to_time” (though effective) is pretty intention-obscuring.
Here’s a clearer, more self-documenting approach, using a named callback:
1 2 3 4 5 6 7 8 9 10 |
class Expense < ActiveRecord::Base before_create :make_created_now_if_created_today protected def make_created_now_if_created_today if self.created_at == Time.now.beginning_of_day self.created_at = Time.now end end end |
The named callback helps make it clearer what the purpose of the method is (though in this case, an additional comment would not be amiss). Also, ActiveSupport comes to the rescue, allowing us to convert the convoluted “Time.now.to_date.to_time” into the more self-documenting “Time.now.beginning_of_day”. (Alternatively, you might prefer “Time.now.midnight”, though I find “beginning_of_day” to be clearer, since it reveals the intention better.)
Always look for ways to make your code document itself. Ruby is one of the most readable programming languages I’ve ever used, and it’s a pity to not take advantage of that readability as often as you can.


Explaining what code does with your method names is always a good thing =].
< sigh >. And here I was a couple of days ago going `man, it’d be nice if named callbacks were supported in Rails’. The ability of Rails developers to read my mind is getting creepy… :-P
Nice example… but, coming from an Oracle background I insist on a given field being a date OR a datetime but not allowing both.
I really like Rails _on and _at to differentiate between date and timestamp fields. Mixing trunc(date) and date_time values is a recipe for losing data in range queries.
Gerard, I agree…but I’m not sure how that’s relevant to this example? There is no mixing of dates and datetimes here. It’s all datetime.
This also alleviates the need to remember to call “super” when doing STI. And you can easily create a chain of callbacks just like you do with filters in controllers, which is kind of nice.
Two questions, in this case, would a different name for the created_at be better, such as incurred_at?
Second, is there a way to pass a variable to a named callback? I’ve dug around a bit and its not obvious to me. If an example would help, let me know.
@planetmcd, yes, incurred_at would probably be better. Reusing the created_at/created_on/updated_at/updated_on columns for anything other than what rails uses them for can lead to obscure bugs.
As for passing variables to callbacks, you cannot. Can you give an example of what you want to do? There’s probably another way to do it.
I find it even clearer to pull the condition into its own self-documenting method. Then you can make the code in the callback method a one-liner that matches the name:
def make_created_now_if_created_today self.created_at = Time.now if self.created_today? end
http://pastie.caboo.se/71396
The comment about needing to call super if you repeat this pattern in subclasses is a good point I hadn’t thought of. Another plus for this approach is that it is much easier (and again, clearer in expression) to test. You can target the specific call back and make sure that it does what it’s supposed to without dancing around any other concerns you might have in your series of callbacks.