One of the little annoyances of ActiveRecord is not being able to override the #initialize
method in your class. There's the #after_initialize
callback, but there's no way to pass an argument to it when you create the record. So what do you do if you want to create a record and initialize it with some stuff that isn't one of its attributes? The answer is, you cheat.
The implementor must cheat, but not get caught. -- Dan Ingalls
The usual way to instantiate a new record is to use #new
and pass a hash of attributes. The cool thing is that the way ActiveRecord assigns those attributes to the new record is easily hackable. For example, consider this:
post = Post.new(:title => "Man bites dog!")
ActiveRecord will set the :title attribute of the new post by using the #title= accessor. Very simple. But that opens a world of possibilities...
class User < ActiveRecord::Base
def full_name=(full_name)
self.first = full_name.split(" ").first
self.last = full_name.split(" ").last
end
end
user = User.new(:full_name => "Monty Python")
That's a pretty simple example. Here's a more standard alternate approach:
class User < ActiveRecord::Base
attr :full_name
def after_initialize
self.first_name = @full_name.split(" ").first
self.last_name = @full_name.split(" ").last
end
end
That's not too bad, but I still think the first example is better. Why muck around with a callback when you don't have to?
Now, check this out.
class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
def init_category=(category)
self.categories << category
end
end
post = Post.new(:init_category => Category.find_by_name("Main"))
The above example shows how to create a new post with a default category set. Since we're using has_and_belongs_to_many
here, we don't have to worry about managing whether the object is a new record since habtm does that for us. But it's a little trickier if we want to use a join model...
class Post < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :tags, :through => :taggings
def tag_list=(tag_string)
if self.new_record?
@tag_string = tag_string
else
self.create_taggings(tag_string) # parse tags, create tags & taggings
end
end
def after_create
self.tag_list = @tag_string if @tag_string
end
end
post = Post.new(:tag_list => "chocolate, dessert, sinful")
has_many :through
won't work with new records, as it needs saved records with actual ids to use in the foreign keys in the join model. This example defers processing the tag_list until a new record is saved, at which point it parses out the tags and creates the taggings to join them to the post.
Tricks like that are why I just love ruby!
I never had a problem with overriding initialize in AR classes:
class A < ActiveRecord::Base def initialize(*args) super(*args) set_defaults end
def set_defaults end
end
The real power with rails is ruby! I wish people got Ruby for Rails and the pickaxe before starting with AWD and #rubyonrails.
Jonathan: I probably should have been more clear about the problems with overriding initialize, but I figured people already knew not to do that. You can get away with it sometimes, but it's risky and won't always do what you expect. For one thing, your approach won't work right if you also use the block syntax for initialized model object, as the code in your subclass initialize method won't have been run yet when the block is executed. I've definitely run into other issues as well, but it's too early in the morning for me to remember all the details (not a coffee achiever).
Interesting - I too have overridden initialize without any problems. It never felt quite right though, and I questioned whether or not I should do it, so I try to use callbacks whenever possible, and I find when I'm forced to think about it carefully, a callback usually handles what I thought I needed to put into initialize. Thanks for the tips on fake_attribute=(stuff), that will come in very handy.
In your second example, doesn't hasandbelongsto_many :categories create a categories=(objects) method, letting you call Post.new(:categories => Category.findby_name("Main")) without any hackery?
Justin: categories= would expect an array of categories, so it wouldn't be as clean...
Josh, once again your blog had the answer to my "How do I best accomplish [x] in Ruby / Rails?" question of the moment. You don't know it but you've been instrumental in my Rails education. I'm working on my first big project in it now; as management sees how quickly development goes with it - and how happy I am - the scope gets ever bigger though, and thus more challenging. Thanks!
Actually based on my experiments in getting formfor and hasmany :through to play nice, that's not strictly true, or am I misunderstanding something?
Tim: I looked at your post. You seem to be faking out hmt by dealing with the join model as a simple has_many. You can certainly do that since has_many works on unsaved objects, but the hmt built on top of that won't work until you've saved the join model records. That may be find for your purposes, but it's not a solution for the general case.
Hmm, I think I might understand. So it's a actually a full hmt, but I am not truly using it there on the creation form. For instance it'd work fine if on a separate edit form I directly called parent.throughmodels, but using the joinmodel.build doesn't actually populate the associated hmt on the parent model - that requires a save?
How about something like this?. I tested and it seems to work. Use the build method in the new to avoid having to use a intermediary storage form and then make a safe accessor for the :through for cases (like the new/edit form) where you can't be sure the collection has been saved yet?
and then I use @report.safe_locations in my edit form. Everywhere else I should be able to use the power of the fully operation :throigh model, yes? Any short-comings I'm missing?
Btw, a preview for comments would be nice, for double-checking Markdown mistakes, like in my earlier post. ;)
Hi guys! Sorry for posting here i have a problem and i don't really know where to ask. I have a form in which users type in excel column indexes such as A,B,C,etc., however i'd like to use these letters as a number i have written a method which converts them into integers. I'd like to use this method as private so i tried to use the model object getter method to invoke this function but i had no success. Here is what i did:
class ImportForm < ActiveRecord::Base def ddestnamecol1 numberfromalpha(@ddestnamecol1) unless @ddestnamecol1.nil? end def numberfromalpha(letter) numeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if(numeric.include?(letter.upcase) && letter.length==1) numeric.index(letter.upcase) else false end end end
@import=ImportForm.new(:ddestnamecol1=>"d")
Result:
if i comment out the getter i've got the string that was enter on the form.
thank in advance,
tomtom