aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/collection_proxy.rb
blob: 8415942a1a9bbc51adbe44cdebe59c5b73152bab (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
    #
    #   blog = Blog.find(:first)
    #
    # 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

      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 <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)
        @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