collection_proxy.rb 4.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
module ActiveRecord
  module Associations
    # Association proxies in Active Record are middlemen between the object that
    # holds the association, known as the <tt>@owner</tt>, and the actual associated
    # object, known as the <tt>@target</tt>. The kind of association any proxy is
    # about is available in <tt>@reflection</tt>. That's an instance of the class
    # ActiveRecord::Reflection::AssociationReflection.
    #
    # For example, given
    #
    #   class Blog < ActiveRecord::Base
    #     has_many :posts
    #   end
    #
A
Akira Matsuda 已提交
15
    #   blog = Blog.first
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
    #
    # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
    # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
    # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
    #
    # This class has most of the basic instance methods removed, and delegates
    # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
    # corner case, it even removes the +class+ method and that's why you get
    #
    #   blog.posts.class # => Array
    #
    # though the object behind <tt>blog.posts</tt> is not an Array, but an
    # ActiveRecord::Associations::HasManyAssociation.
    #
    # The <tt>@target</tt> 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

59 60
      alias_method :new, :build

61 62 63 64
      def proxy_association
        @association
      end

65
      def respond_to?(name, include_private = false)
66
        super ||
67
        (load_target && target.respond_to?(name, include_private)) ||
68
        proxy_association.klass.respond_to?(name, include_private)
69 70 71 72
      end

      def method_missing(method, *args, &block)
        match = DynamicFinderMatch.match(method)
N
Nick Howard 已提交
73
        if match && match.instantiator?
74
          send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
75 76
            proxy_association.send :set_owner_attributes, r
            proxy_association.send :add_to_target, r
N
Nick Howard 已提交
77 78
            yield(r) if block_given?
          end
79 80
        end

81
        if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
          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 <tt>===</tt> 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)
111
        proxy_association.concat(records) && self
112 113 114 115 116 117 118 119 120
      end
      alias_method :push, :<<

      def clear
        delete_all
        self
      end

      def reload
121
        proxy_association.reload
122 123 124 125 126
        self
      end
    end
  end
end