collection_proxy.rb 4.7 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
    #
    # 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,
42
               :lock, :readonly, :having, :pluck, :to => :scoped
43

44
      delegate :target, :load_target, :loaded?, :to => :@association
45 46 47

      delegate :select, :find, :first, :last,
               :build, :create, :create!,
48
               :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq,
49 50 51 52 53 54
               :sum, :count, :size, :length, :empty?,
               :any?, :many?, :include?,
               :to => :@association

      def initialize(association)
        @association = association
55
        Array(association.options[:extend]).each { |ext| proxy_extend(ext) }
56 57
      end

58 59
      alias_method :new, :build

60 61 62 63
      def proxy_association
        @association
      end

64 65 66 67 68 69 70
      def scoped
        association = @association
        association.scoped.extending do
          define_method(:proxy_association) { association }
        end
      end

71
      def respond_to?(name, include_private = false)
72
        super ||
73
        (load_target && target.respond_to?(name, include_private)) ||
74
        proxy_association.klass.respond_to?(name, include_private)
75 76 77 78
      end

      def method_missing(method, *args, &block)
        match = DynamicFinderMatch.match(method)
N
Nick Howard 已提交
79
        if match && match.instantiator?
80
          send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
81 82
            proxy_association.send :set_owner_attributes, r
            proxy_association.send :add_to_target, r
N
Nick Howard 已提交
83 84
            yield(r) if block_given?
          end
85

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

      def clear
        delete_all
        self
      end

      def reload
126
        proxy_association.reload
127 128
        self
      end
129 130 131 132 133 134 135 136 137 138 139 140 141

      # 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
142 143 144
    end
  end
end