Friday, March 28, 2008

Rails: Don't override initialize on ActiveRecord objects

ActiveRecord::Base doesn't always use new to create objects, so initialize might not be called. I wanted to use a Hash on an ActiveRecord::Base subclass to store some calculated values, so I naively did this:
class User < ActiveRecord::Base
  def initialize(args = nil)
    super
    @my_cache = {}
  end
end
However I quickly ran into some "You have a nil object when you didn't expect it!" issues. Some debugger investigation revealed that the @my_cache variable wasn't being set when I called find_or_create_ if the object already existed in the database. Digging in the source revealed that the instantiate method in active_record/base.rb uses allocate to create classes rather than new. This means the initialize method is being neatly sidestepped when creating objects from the database. The solution is to use the 'after_initialize' callback:
class User < ActiveRecord::Base
  def after_initialize
    @my_cache = {}
  end
end
One further note of caution, When passing parameters into a new or create method the after_initialize is called after the parameters have been set. So you can't rely on the initialization being done before overridden accessors are called.

20 comments:

  1. Thanks - this information was just what I needed :)

    ReplyDelete
  2. Thank you Thank you Thank you

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Thank you!! One year old post still benefiting Rails users :)

    ReplyDelete
  5. Thanks for this post.

    Two things:

    1) I had to use 'self.myattribute' instead of '@myattribute' - not sure why (I'm pretty new)

    2) Remember to check whether 'myattribute' is null, otherwise you'll overwrite what is in your db when you recall the object.

    ReplyDelete
  6. Thanks - That's just fixed a very long standing bug of mine - I was using "before_create" but it wasn't getting called early enough.. xxx

    ReplyDelete
  7. @SoccerShoutPhil 1) Not sure, I've not done any Rails for a while. 2) I used this method for calculated values that would not be stored in the database.

    ReplyDelete
  8. You saved the day (looked 4 hours for the error ..)

    ReplyDelete
  9. Thanks for the great description. This saved me a ton of time.

    ReplyDelete
  10. I've found that I need to use both.

    I have an AR object that handles a few different files. I got sick of defining setter methods to handle these files so I created a method to automatically generate the setters using class_eval. Unfortunately, I have to call the method twice. Once in initalize so it works for new objects and once again in after_initalize so it works for updates. In after_initalize I also have to check that the file setter methods haven't been created already so that's additional overhead.

    It all works but I worry that the code is much less clear than I originally intended.

    ReplyDelete
  11. Thanks, that really helps. I mean, I actually needed to do what you said not to do, but the code helped :)

    ReplyDelete
  12. thnaks! thats w0rk3d for me, but
    DEPRECATION WARNING: Base#after_initialize has been deprecated, please use Base.after_initialize :method instead.

    ReplyDelete
  13. You have saved me a day. Thanks.

    ReplyDelete
  14. Thanks -- exactly what I wanted to do and exactly the problem I ran into.

    ReplyDelete
  15. Excellent. That's just what was confusing me, thanks.

    ReplyDelete
  16. This is nice, but you still might need to overwrite ActiveRecord's initialize function. I think your error is due to the fact that the initializer takes a Hash as an argument, which explains you're "You have a nil object when you didn't expect it!". The proper call to override it would be this if I'm not mistaken:

    class User < ActiveRecord::Base
    def initialize(args = {})
    super
    @my_cache = {}
    end
    end

    Then you'll see those error messages go away.

    ReplyDelete
  17. @Unknown Thanks for your comment, I've not looked at the code for a while but the issue I had was that initialize wasn't called at all when active record loaded the object back from the DB.

    ReplyDelete
  18. Seems in Rails 3.2 the syntax has changed:

    class User < ActiveRecord::Base
    after_initialize do
    @my_cache = {}
    end
    end

    worked for me

    ReplyDelete
  19. As "Blog-Name" stated, this is also the case for Rails 3.1

    ReplyDelete