Rails after_initialize only on “new”

2019-03-09 07:30发布

问题:

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

回答1:

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


回答2:

Surely after_initialize :add_product, if: :new_record? is the cleanest way here.

Keep the conditional out of the add_product function



回答3:

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.



回答4:

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?



回答5:

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



回答6:

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.