I have the following 2 models
class Sport < ActiveRecord::Base
has_many :charts, order: "sortWeight ASC"
has_one :product, :as => :productable
accepts_nested_attributes_for :product, :allow_destroy => true
end
class Product < ActiveRecord::Base
belongs_to :category
belongs_to :productable, :polymorphic => true
end
A sport can't exist without the product, so in my sports_controller.rb
I had:
def new
@sport = Sport.new
@sport.product = Product.new
...
end
I tried to move the creation of the product to the sport model, using after_initialize
:
after_initialize :create_product
def create_product
self.product = Product.new
end
I quickly learned that after_initialize
is called whenever a model is instantiated (i.e., from a find
call). So that wasn't the behavior I was looking for.
Whats the way I should be modeling the requirement that all sport
have a product
?
Thanks
Putting the logic in the controller could be the best answer as you stated, but you could get the after_initialize
to work by doing the following:
after_initialize :add_product
def add_product
self.product ||= Product.new
end
That way, it only sets product if no product exists. It may not be worth the overhead and/or be less clear than having the logic in the controller.
Edit: Per Ryan's answer, performance-wise the following would likely be better:
after_initialize :add_product
def add_product
self.product ||= Product.new if self.new_record?
end
Surely after_initialize :add_product, if: :new_record?
is the cleanest way here.
Keep the conditional out of the add_product function
If you do self.product ||= Product.new
it will still search for a product every time you do a find
because it needs to check to see if it is nil or not. As a result it will not do any eager loading. In order to do this only when a new record is created you could simply check if it is a new record before setting the product.
after_initialize :add_product
def add_product
self.product ||= Product.new if self.new_record?
end
I did some basic benchmarking and checking if self.new_record?
doesn't seem to affect performance in any noticeable way.
Instead of using after_initialize
, how about after_create
?
after_create :create_product
def create_product
self.product = Product.new
save
end
Does that look like it would solve your issue?
It looks like you are very close. You should be able to do away with the after_initialize call altogether, but first I believe if your Sport model has a "has_one" relationship with :product as you've indicated, then your Product model should also "belong_to" sport. Add this to your Product model
belongs_to: :sport
Next step, you should now be able to instantiate a Sport model like so
@sport = @product.sport.create( ... )
This is based off the information from Association Basics from Ruby on Rails Guides, which you could have a read through if I am not exactly correct
You should just override initialize method like
class Sport < ActiveRecord::Base
# ...
def initialize(attributes = {})
super
self.build_product
self.attributes = attributes
end
# ...
end
Initialize method is never called when record is loaded from database.
Notice that in the code above attributes are assigned after product is build.
In such setting attribute assignment can affect created product instance.