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

root/tags/rel_2-0-2/activerecord/lib/active_record/associations/has_many_through_association.rb

Revision 8376, 12.2 kB (checked in by rick, 2 years ago)

Ensure that the :uniq option for has_many :through associations retains the order. #10463 [remvee]

Line 
1 module ActiveRecord
2   module Associations
3     class HasManyThroughAssociation < AssociationProxy #:nodoc:
4       def initialize(owner, reflection)
5         super
6         reflection.check_validity!
7         @finder_sql = construct_conditions
8         construct_sql
9       end
10
11       def find(*args)
12         options = args.extract_options!
13
14         conditions = "#{@finder_sql}"
15         if sanitized_conditions = sanitize_sql(options[:conditions])
16           conditions << " AND (#{sanitized_conditions})"
17         end
18         options[:conditions] = conditions
19
20         if options[:order] && @reflection.options[:order]
21           options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
22         elsif @reflection.options[:order]
23           options[:order] = @reflection.options[:order]
24         end
25
26         options[:select]  = construct_select(options[:select])
27         options[:from]  ||= construct_from
28         options[:joins]   = construct_joins(options[:joins])
29         options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
30
31         merge_options_from_reflection!(options)
32
33         # Pass through args exactly as we received them.
34         args << options
35         @reflection.klass.find(*args)
36       end
37
38       def reset
39         @target = []
40         @loaded = false
41       end
42
43       # Adds records to the association. The source record and its associates
44       # must have ids in order to create records associating them, so this
45       # will raise ActiveRecord::HasManyThroughCantAssociateNewRecords if
46       # either is a new record.  Calls create! so you can rescue errors.
47       #
48       # The :before_add and :after_add callbacks are not yet supported.
49       def <<(*records)
50         return if records.empty?
51         through = @reflection.through_reflection
52         raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) if @owner.new_record?
53
54         klass = through.klass
55         klass.transaction do
56           flatten_deeper(records).each do |associate|
57             raise_on_type_mismatch(associate)
58             raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?
59
60             @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(associate)) { klass.create! }
61             @target << associate if loaded?
62           end
63         end
64
65         self
66       end
67
68       [:push, :concat].each { |method| alias_method method, :<< }
69
70       # Removes +records+ from this association.  Does not destroy +records+.
71       def delete(*records)
72         records = flatten_deeper(records)
73         records.each { |associate| raise_on_type_mismatch(associate) }
74
75         through = @reflection.through_reflection
76         raise ActiveRecord::HasManyThroughCantDissociateNewRecords.new(@owner, through) if @owner.new_record?
77
78         load_target
79
80         klass = through.klass
81         klass.transaction do
82           flatten_deeper(records).each do |associate|
83             raise_on_type_mismatch(associate)
84             raise ActiveRecord::HasManyThroughCantDissociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?
85
86             @owner.send(through.name).proxy_target.delete(klass.delete_all(construct_join_attributes(associate)))
87             @target.delete(associate)
88           end
89         end
90
91         self
92       end
93
94       def build(attrs = nil)
95         raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, @reflection.through_reflection)
96       end
97       alias_method :new, :build
98
99       def create!(attrs = nil)
100         @reflection.klass.transaction do
101           self << (object = @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! })
102           object
103         end
104       end
105
106       # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
107       # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
108       # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
109       def size
110         return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
111         return @target.size if loaded?
112         return count
113       end
114
115       # Calculate sum using SQL, not Enumerable
116       def sum(*args, &block)
117         calculate(:sum, *args, &block)
118       end
119      
120       def count(*args)
121         column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
122         if @reflection.options[:uniq]
123           # This is needed because 'SELECT count(DISTINCT *)..' is not valid sql statement.
124           column_name = "#{@reflection.klass.table_name}.#{@reflection.klass.primary_key}" if column_name == :all
125           options.merge!(:distinct => true)
126         end
127         @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
128       end
129
130       protected
131         def method_missing(method, *args, &block)
132           if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
133             super
134           else
135             @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) }
136           end
137         end
138
139         def find_target
140           records = @reflection.klass.find(:all,
141             :select     => construct_select,
142             :conditions => construct_conditions,
143             :from       => construct_from,
144             :joins      => construct_joins,
145             :order      => @reflection.options[:order],
146             :limit      => @reflection.options[:limit],
147             :group      => @reflection.options[:group],
148             :include    => @reflection.options[:include] || @reflection.source_reflection.options[:include]
149           )
150
151           records.uniq! if @reflection.options[:uniq]
152           records
153         end
154
155         # Construct attributes for associate pointing to owner.
156         def construct_owner_attributes(reflection)
157           if as = reflection.options[:as]
158             { "#{as}_id" => @owner.id,
159               "#{as}_type" => @owner.class.base_class.name.to_s }
160           else
161             { reflection.primary_key_name => @owner.id }
162           end
163         end
164
165         # Construct attributes for :through pointing to owner and associate.
166         def construct_join_attributes(associate)
167           join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
168           if @reflection.options[:source_type]
169             join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
170           end
171           join_attributes
172         end
173
174         # Associate attributes pointing to owner, quoted.
175         def construct_quoted_owner_attributes(reflection)
176           if as = reflection.options[:as]
177             { "#{as}_id" => @owner.quoted_id,
178               "#{as}_type" => reflection.klass.quote_value(
179                 @owner.class.base_class.name.to_s,
180                 reflection.klass.columns_hash["#{as}_type"]) }
181           else
182             { reflection.primary_key_name => @owner.quoted_id }
183           end
184         end
185
186         # Build SQL conditions from attributes, qualified by table name.
187         def construct_conditions
188           table_name = @reflection.through_reflection.table_name
189           conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
190             "#{table_name}.#{attr} = #{value}"
191           end
192           conditions << sql_conditions if sql_conditions
193           "(" + conditions.join(') AND (') + ")"
194         end
195
196         def construct_from
197           @reflection.table_name
198         end
199
200         def construct_select(custom_select = nil)
201           selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*"
202         end
203
204         def construct_joins(custom_joins = nil)
205           polymorphic_join = nil
206           if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to
207             reflection_primary_key = @reflection.klass.primary_key
208             source_primary_key     = @reflection.source_reflection.primary_key_name
209             if @reflection.options[:source_type]
210               polymorphic_join = "AND %s.%s = %s" % [
211                 @reflection.through_reflection.table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
212                 @owner.class.quote_value(@reflection.options[:source_type])
213               ]
214             end
215           else
216             reflection_primary_key = @reflection.source_reflection.primary_key_name
217             source_primary_key     = @reflection.klass.primary_key
218             if @reflection.source_reflection.options[:as]
219               polymorphic_join = "AND %s.%s = %s" % [
220                 @reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type",
221                 @owner.class.quote_value(@reflection.through_reflection.klass.name)
222               ]
223             end
224           end
225
226           "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
227             @reflection.through_reflection.table_name,
228             @reflection.table_name, reflection_primary_key,
229             @reflection.through_reflection.table_name, source_primary_key,
230             polymorphic_join
231           ]
232         end
233
234         def construct_scope
235           { :create => construct_owner_attributes(@reflection),
236             :find   => { :from        => construct_from,
237                          :conditions  => construct_conditions,
238                          :joins       => construct_joins,
239                          :select      => construct_select,
240                          :order       => @reflection.options[:order],
241                          :limit       => @reflection.options[:limit] } }
242         end
243
244         def construct_sql
245           case
246             when @reflection.options[:finder_sql]
247               @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
248
249               @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
250               @finder_sql << " AND (#{conditions})" if conditions
251           end
252
253           if @reflection.options[:counter_sql]
254             @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
255           elsif @reflection.options[:finder_sql]
256             # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
257             @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
258             @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
259           else
260             @counter_sql = @finder_sql
261           end
262         end
263
264         def conditions
265           @conditions ||= [
266             (interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]),
267             (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]),
268             (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.source_reflection.options[:conditions])) if @reflection.source_reflection.options[:conditions]),
269             ("#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.name.demodulize)}" unless @reflection.through_reflection.klass.descends_from_active_record?)
270           ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions]  && !@reflection.source_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?)
271         end
272
273         alias_method :sql_conditions, :conditions
274
275         def has_cached_counter?
276           @owner.attribute_present?(cached_counter_attribute_name)
277         end
278
279         def cached_counter_attribute_name
280           "#{@reflection.name}_count"
281         end
282     end
283   end
284 end
Note: See TracBrowser for help on using the browser.