Ruby's Class.allocate and ActiveRecord::Base.find Mar 16

Ever need to create a ruby object, but didn’t want to run any of the code inside of #initialize? Then let me introduce you to Class#allocate:

Allocates space for a new object of *class*’s class and does not call initialize on the new instance. The returned object must be an instance of class.

klass = Class.new do
  def initialize(*args)
    @initialized = true
  end

  def initialized?
    @initialized || false
  end
end

klass.allocate.initialized? #=> false

When would you ever need something like this? When the #initialize method sets state variables that you don’t want set. This is the technique used by ActiveRecord in the find class method.

Now, knowing that ActiveRecord::Base.find returns objects of ActiveRecord::Base, it should theoretically call #new. Then, following this logic, calling #new_record? on any of the objects should return true. But, it doesn’t. It’s false as expected. Let’s look at ActiveRecord::Base.find_by_sql source – all the find_* uses this method at some point or another:

 def find_by_sql(sql)
   connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
 end

It calls #select_all which executes a SQL statement and returns an array of hashes – the hash keys are column names and the values are column values. It then passes each hash in the array to the instantiate class method and returns the collection. Finding the method we see (with my comments added):

 def instantiate(record)
  # find_sti_class determines the proper class to use. In the case
  # of objects that are not STI, then it returns self.
  object = find_sti_class(record[inheritance_column]).allocate

  object.instance_variable_set(:'@attributes', record)
  object.instance_variable_set(:'@attributes_cache', {})

  object.send(:_run_find_callbacks)
  object.send(:_run_initialize_callbacks)

  object
 end

Notice there are no calls to #new. Here is where the #allocate method is called. So instantiate creates a new object using allocate and manually sets the @attributes instance variable. Doing this allows the ActiveRecord::Base to bypass the #initialize method and create objects that do not set the @new_record state variable.

#allocate keeps the code cleaner. If you didn’t have #allocate, you would need to pass an extra argument to let ActiveRecord::Base know that it’s not a new record, call a method after calling #new to set the state variables, or have another initialize method (e.g., init_with_record). All solutions would create unnecessary clutter to ActiveRecord’s interface – which I’m very happy the core team didn’t do!

This was such a lightbulb-above-the-head experience finding the solution that I figure I should share it! Have fun. :)

NOTE: The code snippets above are from edge Rails and may change.