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.creator? attributes = match.attribute_names return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) 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 end end end