module ActiveRecord
module Associations
# 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. The kind of association any proxy is
# about is available in @reflection. That's an instance of the class
# ActiveRecord::Reflection::AssociationReflection.
#
# For example, given
#
# class Blog < ActiveRecord::Base
# has_many :posts
# end
#
# blog = Blog.find(:first)
#
# the association proxy in blog.posts has the object in +blog+ as
# @owner, the collection of its posts as @target, and
# the @reflection object represents a :has_many macro.
#
# This class has most of the basic instance methods removed, and delegates
# unknown methods to @target via method_missing. As a
# corner case, it even removes the +class+ method and that's why you get
#
# blog.posts.class # => Array
#
# though the object behind blog.posts is not an Array, but an
# ActiveRecord::Associations::HasManyAssociation.
#
# The @target object is not \loaded until needed. For example,
#
# blog.posts.count
#
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.
class CollectionProxy # :nodoc:
alias :proxy_extend :extend
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
:lock, :readonly, :having, :to => :scoped
delegate :target, :load_target, :loaded?, :scoped,
:to => :@association
delegate :select, :find, :first, :last,
:build, :create, :create!,
:concat, :delete_all, :destroy_all, :delete, :destroy, :uniq,
:sum, :count, :size, :length, :empty?,
:any?, :many?, :include?,
:to => :@association
def initialize(association)
@association = association
Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
end
def respond_to?(*args)
super ||
(load_target && target.respond_to?(*args)) ||
@association.klass.respond_to?(*args)
end
def method_missing(method, *args, &block)
match = DynamicFinderMatch.match(method)
if match && match.instantiator?
record = send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
@association.send :set_owner_attributes, r
@association.send :add_to_target, r
yield(r) if block_given?
end
end
if target.respond_to?(method) || (!@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
else
scoped.readonly(nil).send(method, *args, &block)
end
end
# Forwards === explicitly to the \target because the instance method
# removal above doesn't catch it. Loads the \target if needed.
def ===(other)
other === load_target
end
def to_ary
load_target.dup
end
alias_method :to_a, :to_ary
def <<(*records)
@association.concat(records) && self
end
alias_method :push, :<<
def clear
delete_all
self
end
def reload
@association.reload
self
end
def new(*args, &block)
if @association.is_a?(HasManyThroughAssociation)
@association.build(*args, &block)
else
method_missing(:new, *args, &block)
end
end
def proxy_owner
ActiveSupport::Deprecation.warn(
"Calling record.#{@association.reflection.name}.proxy_owner is deprecated. Please use " \
"record.association(:#{@association.reflection.name}).owner instead."
)
@association.owner
end
def proxy_target
ActiveSupport::Deprecation.warn(
"Calling record.#{@association.reflection.name}.proxy_target is deprecated. Please use " \
"record.association(:#{@association.reflection.name}).target instead."
)
@association.target
end
def proxy_reflection
ActiveSupport::Deprecation.warn(
"Calling record.#{@association.reflection.name}.proxy_reflection is deprecated. Please use " \
"record.association(:#{@association.reflection.name}).reflection instead."
)
@association.reflection
end
end
end
end