~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb

  • Committer: Richard Lee (Canonical)
  • Date: 2010-10-15 15:17:58 UTC
  • mfrom: (190.1.3 use-case-mapper)
  • Revision ID: richard.lee@canonical.com-20101015151758-wcvmfxrexsongf9d
Merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
module ActiveRecord
2
 
  module Associations
3
 
    # This is the root class of all association proxies:
4
 
    #
5
 
    #   AssociationProxy
6
 
    #     BelongsToAssociation
7
 
    #       HasOneAssociation
8
 
    #     BelongsToPolymorphicAssociation
9
 
    #     AssociationCollection
10
 
    #       HasAndBelongsToManyAssociation
11
 
    #       HasManyAssociation
12
 
    #         HasManyThroughAssociation
13
 
    #            HasOneThroughAssociation
14
 
    #
15
 
    # Association proxies in Active Record are middlemen between the object that
16
 
    # holds the association, known as the <tt>@owner</tt>, and the actual associated
17
 
    # object, known as the <tt>@target</tt>. The kind of association any proxy is
18
 
    # about is available in <tt>@reflection</tt>. That's an instance of the class
19
 
    # ActiveRecord::Reflection::AssociationReflection.
20
 
    #
21
 
    # For example, given
22
 
    #
23
 
    #   class Blog < ActiveRecord::Base
24
 
    #     has_many :posts
25
 
    #   end
26
 
    #
27
 
    #   blog = Blog.find(:first)
28
 
    #
29
 
    # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
30
 
    # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
31
 
    # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
32
 
    #
33
 
    # This class has most of the basic instance methods removed, and delegates
34
 
    # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
35
 
    # corner case, it even removes the +class+ method and that's why you get
36
 
    #
37
 
    #   blog.posts.class # => Array
38
 
    #
39
 
    # though the object behind <tt>blog.posts</tt> is not an Array, but an
40
 
    # ActiveRecord::Associations::HasManyAssociation.
41
 
    #
42
 
    # The <tt>@target</tt> object is not \loaded until needed. For example,
43
 
    #
44
 
    #   blog.posts.count
45
 
    #
46
 
    # is computed directly through SQL and does not trigger by itself the
47
 
    # instantiation of the actual post records.
48
 
    class AssociationProxy #:nodoc:
49
 
      alias_method :proxy_respond_to?, :respond_to?
50
 
      alias_method :proxy_extend, :extend
51
 
      delegate :to_param, :to => :proxy_target
52
 
      instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
53
 
 
54
 
      def initialize(owner, reflection)
55
 
        @owner, @reflection = owner, reflection
56
 
        Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
57
 
        reset
58
 
      end
59
 
 
60
 
      # Returns the owner of the proxy.
61
 
      def proxy_owner
62
 
        @owner
63
 
      end
64
 
 
65
 
      # Returns the reflection object that represents the association handled
66
 
      # by the proxy.
67
 
      def proxy_reflection
68
 
        @reflection
69
 
      end
70
 
 
71
 
      # Returns the \target of the proxy, same as +target+.
72
 
      def proxy_target
73
 
        @target
74
 
      end
75
 
 
76
 
      # Does the proxy or its \target respond to +symbol+?
77
 
      def respond_to?(*args)
78
 
        proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
79
 
      end
80
 
 
81
 
      # Forwards <tt>===</tt> explicitly to the \target because the instance method
82
 
      # removal above doesn't catch it. Loads the \target if needed.
83
 
      def ===(other)
84
 
        load_target
85
 
        other === @target
86
 
      end
87
 
 
88
 
      # Returns the name of the table of the related class:
89
 
      #
90
 
      #   post.comments.aliased_table_name # => "comments"
91
 
      #
92
 
      def aliased_table_name
93
 
        @reflection.klass.table_name
94
 
      end
95
 
 
96
 
      # Returns the SQL string that corresponds to the <tt>:conditions</tt>
97
 
      # option of the macro, if given, or +nil+ otherwise.
98
 
      def conditions
99
 
        @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
100
 
      end
101
 
      alias :sql_conditions :conditions
102
 
 
103
 
      # Resets the \loaded flag to +false+ and sets the \target to +nil+.
104
 
      def reset
105
 
        @loaded = false
106
 
        @target = nil
107
 
      end
108
 
 
109
 
      # Reloads the \target and returns +self+ on success.
110
 
      def reload
111
 
        reset
112
 
        load_target
113
 
        self unless @target.nil?
114
 
      end
115
 
 
116
 
      # Has the \target been already \loaded?
117
 
      def loaded?
118
 
        @loaded
119
 
      end
120
 
 
121
 
      # Asserts the \target has been loaded setting the \loaded flag to +true+.
122
 
      def loaded
123
 
        @loaded = true
124
 
      end
125
 
 
126
 
      # Returns the target of this proxy, same as +proxy_target+.
127
 
      def target
128
 
        @target
129
 
      end
130
 
 
131
 
      # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
132
 
      def target=(target)
133
 
        @target = target
134
 
        loaded
135
 
      end
136
 
 
137
 
      # Forwards the call to the target. Loads the \target if needed.
138
 
      def inspect
139
 
        load_target
140
 
        @target.inspect
141
 
      end
142
 
 
143
 
      def send(method, *args)
144
 
        if proxy_respond_to?(method)
145
 
          super
146
 
        else
147
 
          load_target
148
 
          @target.send(method, *args)
149
 
        end
150
 
      end
151
 
 
152
 
      protected
153
 
        # Does the association have a <tt>:dependent</tt> option?
154
 
        def dependent?
155
 
          @reflection.options[:dependent]
156
 
        end
157
 
 
158
 
        # Returns a string with the IDs of +records+ joined with a comma, quoted
159
 
        # if needed. The result is ready to be inserted into a SQL IN clause.
160
 
        #
161
 
        #   quoted_record_ids(records) # => "23,56,58,67"
162
 
        #
163
 
        def quoted_record_ids(records)
164
 
          records.map { |record| record.quoted_id }.join(',')
165
 
        end
166
 
 
167
 
        def interpolate_sql(sql, record = nil)
168
 
          @owner.send(:interpolate_sql, sql, record)
169
 
        end
170
 
 
171
 
        # Forwards the call to the reflection class.
172
 
        def sanitize_sql(sql, table_name = @reflection.klass.quoted_table_name)
173
 
          @reflection.klass.send(:sanitize_sql, sql, table_name)
174
 
        end
175
 
 
176
 
        # Assigns the ID of the owner to the corresponding foreign key in +record+.
177
 
        # If the association is polymorphic the type of the owner is also set.
178
 
        def set_belongs_to_association_for(record)
179
 
          if @reflection.options[:as]
180
 
            record["#{@reflection.options[:as]}_id"]   = @owner.id unless @owner.new_record?
181
 
            record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
182
 
          else
183
 
            unless @owner.new_record?
184
 
              primary_key = @reflection.options[:primary_key] || :id
185
 
              record[@reflection.primary_key_name] = @owner.send(primary_key)
186
 
            end
187
 
          end
188
 
        end
189
 
 
190
 
        # Merges into +options+ the ones coming from the reflection.
191
 
        def merge_options_from_reflection!(options)
192
 
          options.reverse_merge!(
193
 
            :group   => @reflection.options[:group],
194
 
            :having  => @reflection.options[:having],
195
 
            :limit   => @reflection.options[:limit],
196
 
            :offset  => @reflection.options[:offset],
197
 
            :joins   => @reflection.options[:joins],
198
 
            :include => @reflection.options[:include],
199
 
            :select  => @reflection.options[:select],
200
 
            :readonly  => @reflection.options[:readonly]
201
 
          )
202
 
        end
203
 
 
204
 
        # Forwards +with_scope+ to the reflection.
205
 
        def with_scope(*args, &block)
206
 
          @reflection.klass.send :with_scope, *args, &block
207
 
        end
208
 
 
209
 
      private
210
 
        # Forwards any missing method call to the \target.
211
 
        def method_missing(method, *args)
212
 
          if load_target
213
 
            if @target.respond_to?(method)
214
 
              if block_given?
215
 
                @target.send(method, *args)  { |*block_args| yield(*block_args) }
216
 
              else
217
 
                @target.send(method, *args)
218
 
              end
219
 
            else
220
 
              super
221
 
            end
222
 
          end
223
 
        end
224
 
 
225
 
        # Loads the \target if needed and returns it.
226
 
        #
227
 
        # This method is abstract in the sense that it relies on +find_target+,
228
 
        # which is expected to be provided by descendants.
229
 
        #
230
 
        # If the \target is already \loaded it is just returned. Thus, you can call
231
 
        # +load_target+ unconditionally to get the \target.
232
 
        #
233
 
        # ActiveRecord::RecordNotFound is rescued within the method, and it is
234
 
        # not reraised. The proxy is \reset and +nil+ is the return value.
235
 
        def load_target
236
 
          return nil unless defined?(@loaded)
237
 
 
238
 
          if !loaded? and (!@owner.new_record? || foreign_key_present)
239
 
            @target = find_target
240
 
          end
241
 
 
242
 
          @loaded = true
243
 
          @target
244
 
        rescue ActiveRecord::RecordNotFound
245
 
          reset
246
 
        end
247
 
 
248
 
        # Can be overwritten by associations that might have the foreign key
249
 
        # available for an association without having the object itself (and
250
 
        # still being a new record). Currently, only +belongs_to+ presents
251
 
        # this scenario (both vanilla and polymorphic).
252
 
        def foreign_key_present
253
 
          false
254
 
        end
255
 
 
256
 
        # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
257
 
        # the kind of the class of the associated objects. Meant to be used as
258
 
        # a sanity check when you are about to assign an associated record.
259
 
        def raise_on_type_mismatch(record)
260
 
          unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
261
 
            message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
262
 
            raise ActiveRecord::AssociationTypeMismatch, message
263
 
          end
264
 
        end
265
 
 
266
 
        # Array#flatten has problems with recursive arrays. Going one level
267
 
        # deeper solves the majority of the problems.
268
 
        def flatten_deeper(array)
269
 
          array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
270
 
        end
271
 
 
272
 
        # Returns the ID of the owner, quoted if needed.
273
 
        def owner_quoted_id
274
 
          @owner.quoted_id
275
 
        end
276
 
    end
277
 
  end
278
 
end