This week has been hectic… I’ve been tasked with implementing a large feature in a short amount of time (isn’t it always that way?).
At any rate, I’ve come to one conclusion about ActiveRecord Observers.
The recommended way doesn’t make sense to me and can actually break.
Observers are meant to be classes that encapsulate functionality that might not be the responsibility of the class it’s observing, but it needs to take action when the class saves, updates, deletes, etc.
But yeah, it breaks in my certain scenario… and I’ve been thinking on how it could be done better.
But that’s one of the nice things about Rails. Since I’ve started working full time with Rails, I’ve dug deep into the internal code and figured out how I could accomplish what I wanted to.
Imagine the following scenerio:
class User < ActiveRecord::Base
has_many :comments, :conditions => ['comment_status_id = ?', CommentStatus[:approved]]
end
class UserObserver < ActiveRecord::Observer
observer User
def after_save(user)
# do some funky logic when a user saves.
end
end
Typically… (according to standard documentation)… you would register your UserObserver in your #{RAILS_ROOT}/config/environment.rb
config.active_record.observers = :user_observer
I don’t like this…
For two reasons:
- It’s confusing on where it gets wired up. A developer comes in, something weird happens when they save their User. Where would they go first? User… then some sort of controller… and if you eventually knew that it was an observer, you’d look at UserObserver. Never config/environment.rb
- This doesn’t work for tests when your Class requires data to be in the database… as in the given example. I’ll tell you why when I explain how observers work.
Well… here’s what happens when you type the magical line:
config.active_record.observers = :user_observer
ActiveRecord::Base class will iterate the symbols and classes you feed into observers=(*observers) and it will instantiate each one of the classes. Each one of the observers then registers itself with the class it observers (that little observer User line in UserObserver). This then causes that class to load.
Here’s the problem for tests… This stuff is happening BEFORE your test fixtures have a chance to get into the database… It will load your environment (config/environment.rb) long before it brings in data… so when it parses the observer UserObserver, which observers User, which in turn loads the class User… which causes the has_many to be parsed and executed… which requires that CommentStatus[:approved] to be solved, which goes to the database… to get the comment status of ‘approved’… which isn’t loaded yet from fixtures. You get the idea.
What’s the better approach?
Since observers=(*observers) is JUST instantiating instances of the observer, and it in turn instantiates the observed class… why not do this?
class User < ActiveRecord::Base
ActiveRecord::Base.observers = :user_observer
has_many :comments, :conditions => ['comment_status_id = ?', CommentStatus[:approved]]
end
This makes sense!
- It gets loaded ONLY when the User class gets loaded… and it will happen after your environment is loaded and your fixtures get into the database.
- It allows you to easily remove the observer…
- It also makes it easier to find… you’ve essentially led the user right to the User model to find the observer.
The only thing I don’t like about this approach is that the User is knowledgeable about it’s observers… but in this case, I think it’s perfectly acceptable.
What do you guys think? Are there any caveats that I’m not aware of?