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.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, :pluck, :to => :scoped delegate :target, :load_target, :loaded?, :to => :@association delegate :select, :find, :first, :last, :build, :create, :create!, :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, :sum, :count, :size, :length, :empty?, :any?, :many?, :include?, :to => :@association def initialize(association) @association = association Array(association.options[:extend]).each { |ext| proxy_extend(ext) } end alias_method :new, :build def proxy_association @association end def scoped(options = nil) association = @association scope = association.scoped scope.extending! do define_method(:proxy_association) { association } end scope.merge!(options) if options scope end def respond_to?(name, include_private = false) super || (load_target && target.respond_to?(name, include_private)) || proxy_association.klass.respond_to?(name, include_private) end def method_missing(method, *args, &block) match = DynamicMatchers::Method.match(method) if match && match.is_a?(DynamicMatchers::Instantiator) scoped.send(method, *args) do |r| proxy_association.send :set_owner_attributes, r proxy_association.send :add_to_target, r yield(r) if block_given? end 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 else scoped.readonly(nil).public_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) proxy_association.concat(records) && self end alias_method :push, :<< def clear delete_all self end def reload proxy_association.reload self end # Define array public methods because we know it should be invoked over # the target, so we can have a performance improvement using those methods # in association collections Array.public_instance_methods.each do |m| unless method_defined?(m) class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{m}(*args, &block) target.public_send(:#{m}, *args, &block) if load_target end RUBY end end end end end