среда, апреля 05, 2006

ActiveRecord observers and STI

Last couple of days I spent my time trying to figure out, why my AR model observers are not working.

I've got several models (actually, different user types) that shared the same table through Single Table Inheritance. So, I've got User and Member models (where Member inherit from User).

I've also got an observer for User model (a UserObserver) that should send a notification email after User has registered. Then I've made a form for Member registration and was confused by the fact that my observer callbacks didn't get triggered every time I register new user.

After digging Rails internals, I've found out that my understanding of how observers work was not correct: when observer was registered for particular class, it traverses that class and all it's subclasses and applies callback hooks to each of them. The problem was that when observer is applied, my Member model is not loaded yet (and thus it had no callback hooks). So, I put "require 'member'" at the end of my "user.rb" and it worked.

So, it worked first but soon broke. After another digging Rails internals I figured out, that the problem was in class caching. In development mode, reloadable classes (that are controller, model and some other classes) are reloaded every request. BUT, observers are not applied after the model has been reloaded.

Then I went digging Rails on how class reload is accomplished and found out that there were pretty nice schema: ActiveSupport package provided simple dependency loading by implementing custom "const_missing" method. Every time user reference some class that wasn't loaded yet, the "const_missing" is called and that class got loaded.

ActionController also had some dependency tricks like "model ...", "observer ..." etc class methods. So, I set up "observer :user_observer" in the ApplicationController to force observer to be applied on every reload:

class ApplicationController < ActionController::Base
observer :user_observer
end


This would be sufficient if there were no STI thing: UserObserver observer get loaded and reference User model, which is loaded then (but it appears that Member model doesn't registered as User subclass at that time)..

So, after all that the solution was to include "model :user" before "observer :user_observer" in the ApplicationController:

class ApplicationController < ActionController::Base
model :user
observer :user_observer
end