Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/activerecord/lib/active_record/associations/association_proxy.rb

Revision 9230, 6.4 kB (checked in by pratik, 3 months ago)

Refactor HasManyThroughAssociation to inherit from HasManyAssociation. Association callbacks and <association>_ids= now work with hm:t. Closes #11516 [rubyruy]

Line 
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_)/ }
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       def proxy_owner
61         @owner
62       end
63
64       def proxy_reflection
65         @reflection
66       end
67
68       def proxy_target
69         @target
70       end
71
72       def respond_to?(symbol, include_priv = false)
73         proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
74       end
75
76       # Explicitly proxy === because the instance method removal above
77       # doesn't catch it.
78       def ===(other)
79         load_target
80         other === @target
81       end
82
83       def aliased_table_name
84         @reflection.klass.table_name
85       end
86
87       def conditions
88         @conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
89       end
90       alias :sql_conditions :conditions
91
92       def reset
93         @loaded = false
94         @target = nil
95       end
96
97       def reload
98         reset
99         load_target
100         self unless @target.nil?
101       end
102
103       def loaded?
104         @loaded
105       end
106
107       def loaded
108         @loaded = true
109       end
110
111       def target
112         @target
113       end
114
115       def target=(target)
116         @target = target
117         loaded
118       end
119
120       def inspect
121         reload unless loaded?
122         @target.inspect
123       end
124
125       protected
126         def dependent?
127           @reflection.options[:dependent]
128         end
129
130         def quoted_record_ids(records)
131           records.map { |record| record.quoted_id }.join(',')
132         end
133
134         def interpolate_sql_options!(options, *keys)
135           keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
136         end
137
138         def interpolate_sql(sql, record = nil)
139           @owner.send(:interpolate_sql, sql, record)
140         end
141
142         def sanitize_sql(sql)
143           @reflection.klass.send(:sanitize_sql, sql)
144         end
145
146         def set_belongs_to_association_for(record)
147           if @reflection.options[:as]
148             record["#{@reflection.options[:as]}_id"]   = @owner.id unless @owner.new_record?
149             record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
150           else
151             record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
152           end
153         end
154
155         def merge_options_from_reflection!(options)
156           options.reverse_merge!(
157             :group   => @reflection.options[:group],
158             :limit   => @reflection.options[:limit],
159             :offset  => @reflection.options[:offset],
160             :joins   => @reflection.options[:joins],
161             :include => @reflection.options[:include],
162             :select  => @reflection.options[:select],
163             :readonly  => @reflection.options[:readonly]
164           )
165         end
166
167         def with_scope(*args, &block)
168           @reflection.klass.send :with_scope, *args, &block
169         end
170          
171       private
172         def method_missing(method, *args)
173           if load_target
174             if block_given?
175               @target.send(method, *args)  { |*block_args| yield(*block_args) }
176             else
177               @target.send(method, *args)
178             end
179           end
180         end
181
182         def load_target
183           return nil unless defined?(@loaded)
184
185           if !loaded? and (!@owner.new_record? || foreign_key_present)
186             @target = find_target
187           end
188
189           @loaded = true
190           @target
191         rescue ActiveRecord::RecordNotFound
192           reset
193         end
194
195         # Can be overwritten by associations that might have the foreign key available for an association without
196         # having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
197         def foreign_key_present
198           false
199         end
200
201         def raise_on_type_mismatch(record)
202           unless record.is_a?(@reflection.klass)
203             raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.klass} expected, got #{record.class}"
204           end
205         end
206
207         # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
208         def flatten_deeper(array)
209           array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
210         end
211     end
212   end
213 end
Note: See TracBrowser for help on using the browser.