Ruby's Class.allocate and ActiveRecord::Base.find
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.
3 comments
Mark Wilden Mar 17, 2010
It seems to me that this code is necessary because a new ActiveRecord is not necessarily a new_record?
#initialize assumes it is a new_record? which is just wrong.
Creating a new ActiveRecord object in memory should have nothing to do with creating a new row in a database table.
So this seems to me to be a non-OOP hack. Whether the hack is necessary is another question - maybe it is.
Justin Blake Mar 19, 2010
I don't think this is a hack. This essentially makes .find a special kind of .new. You're not really creating a new object when it's state already exists in the database; you're fetching an existing object, so it makes sense that you wouldn't want #initialize to run. At least to me.
Mark Wilden Mar 23, 2010
I think #new is orthogonal to persistence. Every OOP language that I know of has something like #new to 1) allocate storage, and 2) initialize an object. That's it. It doesn't matter if the real-world entity is "new" (like a baby) or if there's a new artifact in the persistence mechanism (as the result of a SQL INSERT statement very soon before or after #new is called).
In my experience, doing strange things like calling Class.allocate arises from three possible situations:
1) You are doing something different from what everyone else is doing.
2) You've discovered something revolutionary that everyone else would be wise to adopt.
3) You're doing it wrong.
My vote here is with #3. A new object has nothing to do with a new database row, so I don't think the other two possibilities apply.
But I don't know all the details of the decision, so I could always be wrong. :)
Add a Comment