Friday, 28 March 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.

21 comments:

Casper Fabricius said...

Thanks - this information was just what I needed :)

Sxadvi said...

Thank you Thank you Thank you

Livet är en kod. said...
This comment has been removed by the author.
kulpae said...

Thank you!

Abhijit Dixit said...

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

SoccerShoutPhil said...

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.

Ian Moody said...

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

Dale said...

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

Take -IT- Easy said...

Thanks a lot!!!!!!

Oliver Haag said...

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

Stephen said...

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

steven said...

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.

Daniel Rosenstark said...

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

lessless said...

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

arkadi.kagan said...

You have saved me a day. Thanks.

stz said...

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

Rob said...

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

Unknown said...

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.

Dale said...

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

Blog-Name said...

Seems in Rails 3.2 the syntax has changed:

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

worked for me

curtis foster said...

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