How does ActiveRecord work?

(written by lawrence krubner, however indented passages are often quotes). You can contact lawrence at: lawrence@krubner.com

Interesting:

Because find(id) is such a common query, the ActiveRecord developers have special-cased it to bypass a lot of the work that would have to be done if you were chaining together conditions or doing more elaborate queries. You can see at line 147 that this simple query is actually cached, and that this cache is checked to see if the query has been executed before.

If not, the query construction begins at line 149, with the chaining of a where clause and a limit clause. This query is then executed at line 152, and the first result is returned.

The code on line 145:

key = primary_key
s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
where(key => params.bind).limit(1)
}
}

And then we move into the where clause:

Here, inside the where method, we see that, again, there’s more complexity than one might expect, because there’s a lot of magic in ActiveRecord argument processing!

There are three branches here, two of which handle no-op cases: when called with no arguments (i.e. where()), the first branch is executed, creating a new WhereChain instance. We’re not going to spend much time on WhereChain here, but you should know that among other things, it is what allows negating a query using the not method:

Person.where.not(age: 18)

WhereChain actually does a lot of interesting stuff, and I encourage you to take a look at it. But for now, we’re going to follow the path that matters to our example: the third branch of this, to the call to where!. This ends up doing some less-interesting stuff involving argument processing, and then it calls build_where to do the dirty work…so let’s skip straight there, instead.

The code:

def where(opts = :chain, *rest)
if opts == :chain
WhereChain.new(spawn)
elsif opts.blank?
self
else
spawn.where!(opts, *rest)
end
end

And then the code that builds the query:

We’re in the process of chasing down a very simple query (Person.find(1)) to see what happens inside ActiveRecord. Remember that in previous steps, our simple query was transformed to a more complex query, in that Person.find() became Person.where(1), which then became a where(primary_key=>id).

There’s a bunch of logic here that would take us all over the code if we chased it, but let’s see if we can simplify: resolve_column_aliases does pretty much what it sounds like, and isn’t critical to our current mission, so we’ll skip over it.

We know that PredicateBuilder.build_from_hash must be interesting, since it’s the return value, but what about the lines in the middle? The call to expand_hash_conditions_for_aggregates is for magic related to attributes declared with composed_of, but we don’t care about that for our current example. So we’re left with create_binds, and build_from_hash. As it turns out create_binds is pretty important. Let’s dig into that method.

The code:

when Hash
opts = PredicateBuilder.resolve_column_aliases(klass, opts)
tmp_opts, bind_values = create_binds(opts)
self.bind_values += bind_values
attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
add_relations_to_bind_values(attributes)
PredicateBuilder.build_from_hash(klass, attributes, table)
else
[opts]
end

Off-topic: I just spent 3 months working on a proprietary system written in Python, and I had to do all of this kind of analysis on my own. There is something pleasant about using a well known public framework, where there is a whole community doing analysis of it.

Source