Space Vatican

Ramblings of a curious coder

Do You Know When Your Code Runs?

Few things are more head bashing inducing than code that passes all unit tests, runs perfectly on your development machine but fails on your staging/production servers. In that vein, both of these examples are wrong:

1
2
3
4
5
6
7
8
9
10
class Person < ActiveRecord::Base
  has_many :posts
  has_many :recent_posts, :class_name => 'Post', :conditions => ["created_at > ?", 1.week.ago]
  validates_inclusion_of :birth_date, :in => (20.years.ago..13.years.ago),
                            :message => "You must be a teenager to signup", :on => :create
end

class Post < ActiveRecord::Base
  named_scope :recent, :conditions => ["created_at > ?", 1.week.ago]
end

In development mode this will work absolutely fine. When you deploy this code onto the production server it will work fine too, but after a while it won’t behave quite right. For example Person.recent_posts will start returning posts older than 1 week.

The key to this is understanding when the code runs. In particular when does “1.week.ago” get turned into an instance of Time with some fixed value such as 1st November 2008 at 20:32?

The statements has_many, validates_inclusion_of etc… are just method calls, so their arguments are evaluated when that function is called. You can look in the options hash for an association to see this (assuming you’ve just typed in the Person class given above):

1
2
Person.reflections[:recent_posts].options
=> {:conditions=>["created_at > ?", Sun Nov 02 14:27:26 +0000 2008]}

So when are these functions called? Quite simply when ruby loads person.rb. In development mode your source code is reloaded for each request[1], providing the illusion that the “1.week.ago” is re-evaluated whenever it is used. In production mode person.rb would only be read once per Rails instance and so once your mongrels had been running for a week Post.recent_posts would return anything written in the last 2 weeks (1 week before the date at which your mongrels were launched). You would also notice this if you were running script/console and keeping an eye on the sql generated: you’d see that the date in the WHERE clause didn’t change.

Fixing it.

Fortunately it’s not hard to fix this. In this case of the awesome named_scope you probably already know that you can supply a Proc for when you want your scope to take arguments. We can equally make one with no arguments, just to ensure that the time condition is evaluated whenever the scope is accessed.

1
2
3
class Post
  named_scope :recent, lambda { {:conditions => 1.week.ago}}
end

For conditions on things like associations we can use a little trick called interpolation. As I’m sure you know when ruby encounters “#{ ‘hello world’ }” it evaluates the things inside the #{}, but if you use single quotes (or equivalently things like %q() then it doesn’t. What you may not know is that Active Record will perform that interpolation again at the point where sql is generated. For example we can write the recent posts associations like this:

1
2
3
4
class Person < ActiveRecord::Base
  has_many :recent_posts, :class_name => 'Post',
           :conditions => 'created_at > #{self.class.connection.quote 1.week.ago}'
end

When person.rb is loaded the stuff in the #{} will not be evaluated, however when Active Record generates the sql needed to load the association it will be[2].

Validations can’t play any of the clever little games that the other 2 examples can. You’ll just have to something like

1
2
3
4
5
6
7
8
9
class Person < ActiveRecord::Base
  validate_on_create :is_a_teenager

  def is_a_teenager
    unless birth_date < 13.years.ago && birth_date > 20.years.ago
      ...
    end
  end
end

[1] Assuming you’ve got config.cache_classes set to false in development mode which is the default

[2] You can do a lot more with interpolation. Normally the code is interpolated in the context of the instance of the model so you can use any model methods, instance variables etc… When an association is fetched with :include it will be interpolated in the context of the class (since the whole point is to bulk load instances it does not make sense (nor would it work) to work per instance data in there.