ActiveRecord Observers

Posted by Jake Good
on Jan 26, 07

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:




  1. 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

  2. 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?

Comments

Leave a response

  1. Luke FranclJan 26 07 @ 04:18PM

    I had a similar problem with observers this week.



    I was using a plugin that called self.columns on the model class. But this broke when you tried to create a new database, because as you note in your post, setting up observers in your environment file causes the model class to get loaded. But when the model tries to look at its columns and the database table doesn’t exist yet…BOOM.

  2. M.Jan 26 07 @ 04:18PM

    Man, I don’t know what the f@$k you just said little kid, but you special man… reach out, touch a brother heart.



    – Tracey Morgan, Jay and Silent Bob Strike Back.

  3. BrianJan 26 07 @ 04:18PM

    Nice site.. a friend of mine stumbled on this.



    Well, I am not sure what you’re trying to do, but I just recreated your scenario and had no problems running unit tests. It sounds like you have some special conditions in your code that’s causing you some frustration but I don’t think it has to do with observers, since I use them all the time.



    I’d check your fixtures, and also look into how you’re running your tests. Fixtures should load directly into the db, not through the models. Also, if you really are having problems, load the observers in the dev and prod environments only, then load them in your test environment using a test helper.



    I also recommend not using fixtures in your tests; instead, write test helper methods that build up your objects directly. These can also contain assertions you can use to test validations, etc. It’s much easier to track when your database gets large.



    I will say that I am interested in your problem. Hit me up at my email address if you have more code you can use to explain your problem and the errors you’re getting.

  4. Jake GoodJan 26 07 @ 04:18PM

    @Brian - the interesting thing is, if you have previously ran rake test and your fixtures were loaded, then you wouldn’t have a problem… because the class loading and the eventual requirement of data will be fulfilled due to a previous rake test.



    I’m pretty sure, through working my way through the Rails code… that it’s purely a timing issue with what gets loaded when…



    I will definitely hit you up.

  5. Jake GoodJan 26 07 @ 04:18PM

    @Mike - Thanks man!

  6. Michael MahemoffJan 26 07 @ 04:18PM

    I just found this post as I had a similar problem to Luke, where my active record class relies on a plugin, so when I try the standard config idiom in environment.rb, I get an error because it must load the active record class. So it looks like your technique has an additional benefit as a way to untangle the dependency loop.

  7. Jake GoodJan 26 07 @ 04:18PM

    @Michael - It’s definitely something that needs to be looked at in Rails. I’ve continually been hammered with questions about the timing of dependency loading and how things differ when you’re actually pushing through the Rails pipeline or just running tests.



    I’m sure it’s documented somewhere, but there needs to be a pretty easy clean picture drawn for people to know how to tap into the loop.

Comment