On When Create Is Not Like Create

July 14, 2011 § Leave a comment


I recently ran into an interesting issue in ActiveRecord while trying to set default values on an object using the after_initialize callback.  One would think the following blocks of code would be equivalent:

# new, save
Product.new(:name => "Awesome Product").save

# new w/ block, save
product = Product.new do |p|
  p.title = "Awesome Product"
end
product.save

# create
Product.create(:title => "Awesome Product")

# create w/ block
Product.create do |p|
  p.title = "Awesome Product"
end

In 99.9% of cases this is going to be true.  The last one, create with a block, however, can potentially cause you some problems if you are using after_initialize. The problem arises when you use after_initialize to set default values for attributes are are dependent on other attributes. Let us consider our Awesome Product has two more attributes, msrp and wholesale_price, that are tied to each other. If we have one of them we can always determine what the other should be. In this case, there wouldn’t really be a reason set both of them when creating a new object. Just set one and let the other one get set automatically.

For our example we’ll say, msrp = 2 * wholesale_price. You might use an after_initialize that looks something like this:

def after_initialize
  # set wholesale_price based on msrp
  if !msrp.nil? && wholesale_price.nil?
    self.wholesale_price = msrp / 2
  # set msrp based on wholesale_price
  elsif msrp.nil? && !wholesale_price.nil?
    self.msrp = wholesale_price * 2
  end
end

We can instantiate an object like this:

product = Product.new(:name => "Awesome Product", :msrp => 20)
 => <Product ...>
product.save
 => true
product.msrp
 => 20
product.wholesale
 => 10

Everything is working as it should. Now let’s use create instead of new and save.

product = Product.create(:name => "Awesome Product", :msrp => 20)
 => true
product.msrp
 => 20
product.wholesale
 => 10

Still works just fine. Now create with a block:

product = Product.create do |p|
  p.name = "Awesome Product"
  p.msrp => 20
end
 => true
product.msrp
 => 20
product.wholesale
 => nil

Uh, oh… Why didn’t wholesale_price didn’t get set? Take a look at the implementation of create in ActiveRecord::Base.

def create(attributes = nil, &block)
  if attributes.is_a?(Array)
    attributes.collect { |attr| create(attr, &block) }
  else
    object = new(attributes)
    yield(object) if block_given?
    object.save
    object
  end
end

Notice in the else block that a new object is created and then the block is yielded. This means that the after_initialize callback is run on the instantiated object BEFORE the block code is run. msrp is not set yet when after_initialize is run, so wholesale_price can’t be set. create without a block work fine because it is literally the same as using new and save.

TL;DR – after_initialize runs before the block code when using create and a block. Be careful when using after_initialize to set default values for attributes that depend on other attributes.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

What’s this?

You are currently reading On When Create Is Not Like Create at The On Blog.

meta

%d bloggers like this: