Taking a peek at Active Record Proxies

If you've worked in a Rails project before, you've undoubtedly set up associations beteween your models. Active Record provides a clean and simple way in setting up these associations, while hiding a lot of the complexity behind the scenes.

One thing that I found interesting is how method calls are forwarded between associated models. Let's take a look at a simple example:

class Team < ActiveRecord::Base
  has_many :players
end

class Players < ActiveRecord::Base
  belongs_to :team
end

When we say team.players, what are we really working with? One's first reaction might be to inspect it's class. When inspecting team.players, we get back an Array, but Rails is intentionally lying to us. It's actually an Active Record class called CollectionProxy!

Association Proxies

So what exactly is this proxy class? From the Rails documentation:

Association proxies in Active Record are middlemen between the object that holds the association, known as the @owner, and the actual associated object, known as the @target.

In our case, the @owner is the team while the @target is the players collection.

Back to the original question, how does team.players.class return an Array? The CollectionProxy class does some metaprogramming to make the magic happen.

First it undefines it's own #class method using undef_method

# Rails 3.2
# Associations::ActiveRecord::CollectionProxy

instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }

Secondly, it implements #method_missing to delegate unknown methods to the @target, or in our case, the players array.

# Rails 3.2
# Within def method_missing(method, *args, &block)

elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
  if load_target
    if target.respond_to?(method)
      target.send(method, *args, &block)
    else
      begin
        super
      rescue NoMethodError => e
        raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
      end
    end
  end

Therefore, our call to class will be picked up by method_missing.

By having this proxy class, ActiveRecord can hook into certain methods and determine whether to perform database queries or used cache records.

by Ryan Billings