views:

65

answers:

1

It seems that when a child object has a reference to its parent in its setter, it fails to get initialized unless the foreign key is given first in the parameter hash.

class Bot < ActiveRecord::Base
  has_many :items
end

class Item < ActiveRecord::Base
  belongs_to :bot

  def name=(text)
    write_attribute(:name, "#{self.bot.name}'s #{text}")
  end
end

Item.new(:name => 'pitchfork', :bot_id => 1, ... )
# => undefined method `name' for nil:NilClass (NoMethodError)
Item.new(:bot_id => 1, :name => 'pitchfork', ... )
# => #<Item id: nil, bot_id: 1, name: "r2d2's pitchfork", ... >

Note that the order of hash keys is preserved in Ruby 1.9, but the point is, bot_id must be set before the accessor that has a reference to its parent.

So, the following code works too:

item = Item.new
item.bot_id = 1
item.attributes = { :name => 'pitchfork', ... }

What's really annoying is that the build method on has_many collection doesn't work either, which I think is the right place to patch if I'd have to.

Bot.find(1).items.build(:name => 'pitchfork')
# => undefined method `name' for nil:NilClass (NoMethodError)

What's the best idea to get around this, or patch this, or am I doing anything wrong here?

A: 

You could move the string merging to an after_update callback. That way you won't have to access the Bot model until after it's properly setup.

However, I would probably keep name as a simple string and then add a virtual attribute for the merged string. That way it's also updated if the name of Bot is changed.

class Item < ActiveRecord::Base
  belongs_to :bot

  def full_name
    @full_name ||= "#{bot.name}'s #{name}"
  end
end
ksoderstrom
I guess my simplification in this question was inappropriate... I should have put more stress on mass assignment. In my real app, there are multiple, different kind of setters that try to find other models using bot_id, doing various types of tasks, then save some values to its own attribute. I'd go with virtual attributes as you said otherwise.As to after_update (after_create, you mean?), I gave it a try but there are just too many attributes that need bot object and after_create quickly got messy. All I want is bot_id to be set first transparently in mass assignment, after all...
kenn
I found a way to get around this problem- if you want to make sure some parameters come first in a hash, you could use `reverse_merge`, like so: Item.new params.reverse_merge(:bot_id => 1)I think it is good enough for now.
kenn