Rails has a lovely feature as part of ActiveRecord - Callbacks; to wit:
Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic before or after an alteration of the object state
Basically, that means that you can instruct Rails to execute your own code throughout the process of creating, updating and destroying objects/records. You can, for example, use them to set some default values on create, or perhaps do some last minute validation, or even fire an event just after you've saved a record. The available callbacks are:
- after_initialize
- after_find
- after_touch
- before_validation
- after_validation
- before_save
- around_save
- after_save
- before_create
- around_create
- after_create
- before_update
- around_update
- after_update
- :before_destroy
- around_destroy
- after_destroy
- after_commit
- after_rollback
At the same time, Ruby methods have the concept of implicit return values. That is to say, any method will return the value of the last evaluated statement. This is neat - you don't need to write an explicit "return" at the end of your methods.
When we mangle these two features together, we can create subtle bugs in our code. Well not so subtle, because your code will just stop working the way you expect, and it can be fairly difficult to debug because no error is thrown.
Consider the Rails Callbacks again:
If a callback returns false, all the later callbacks and the associated action are cancelled
And I'm sure you'll see where I'm going with this now.....Take an example where you have a boolean value as one of your fields in your model. You want to set this boolean in a callback, either as a default value or because it represents some state based on some other logic.
Let's contrive an example - a blog application which allows comments. We want comments to be moderated of course, because spam is bad. We create a quick boolean on our comment model called "isinmoderation". Every time a new comment is submitted, we want to set this variable to true. We have an added piece of logic to trust any comments from readers who have submitted five good (non-spammy) comments.
Given the above, we might end up with something like the following:
class Comment < ActiveRecord::Base before_create :moderate protected def moderate unless is_from_trusted_reader this.isinmoderation = false end end end
We run some tests and find that new comments from untrusted readers are not being saved to the database. We notice that our transaction is being rolled back, but there's no error. For a while, we are confused. And then we remember this blog post and realise that we are returning a falsey value from one of our callbacks....and, as the Rails docs say,
all the later callbacks and the associated action are cancelled
And the mystery is solved. So is there an elegant way to ensure we don't hit this condition? Well one way out is to explicitly declare a return of true
from our moderate
method. It is a little less elegant than the normal implicit return, but c'est la vie
Comments
Post a Comment