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!
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.