Tyler DeWitt
Tyler DeWitt

Reputation: 23576

Rails after_initialize only on "new"

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

Upvotes: 67

Views: 53427

Answers (8)

tee_zed_0
tee_zed_0

Reputation: 43

before_create is perfect for this particular case.

You see, if you want to initiate some block of code on nearly created record you should use before_create hook because it would be called only once - when record has been created. No on update neither on save. Only once - when record is created.

my_model = Model.create(**record_params) # this will trigger before_create hook
my_model.update(**some_params) # this will not trigger before_create hook 

Upvotes: 0

after_initialize :add_product, unless: :persisted?

Upvotes: 3

Ryan
Ryan

Reputation: 9430

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.

Upvotes: 33

Paul Odeon
Paul Odeon

Reputation: 4524

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

Keep the conditional out of the add_product function

Upvotes: 71

Victor Nazarov
Victor Nazarov

Reputation: 835

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.

Upvotes: 0

bostonou
bostonou

Reputation: 1194

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

Upvotes: 72

coderates
coderates

Reputation: 270

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

Upvotes: 1

James Brooks
James Brooks

Reputation: 658

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?

Upvotes: 2

Related Questions