ad375be1842ba48aa96d451b9bd55aad3455d079
[feedcatcher.git] / vendor / rails / activerecord / lib / active_record / associations / association_collection.rb
1 require 'set'
2
3 module ActiveRecord
4 module Associations
5 # AssociationCollection is an abstract class that provides common stuff to
6 # ease the implementation of association proxies that represent
7 # collections. See the class hierarchy in AssociationProxy.
8 #
9 # You need to be careful with assumptions regarding the target: The proxy
10 # does not fetch records from the database until it needs them, but new
11 # ones created with +build+ are added to the target. So, the target may be
12 # non-empty and still lack children waiting to be read from the database.
13 # If you look directly to the database you cannot assume that's the entire
14 # collection because new records may have beed added to the target, etc.
15 #
16 # If you need to work on all current children, new and existing records,
17 # +load_target+ and the +loaded+ flag are your friends.
18 class AssociationCollection < AssociationProxy #:nodoc:
19 def initialize(owner, reflection)
20 super
21 construct_sql
22 end
23
24 def find(*args)
25 options = args.extract_options!
26
27 # If using a custom finder_sql, scan the entire collection.
28 if @reflection.options[:finder_sql]
29 expects_array = args.first.kind_of?(Array)
30 ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
31
32 if ids.size == 1
33 id = ids.first
34 record = load_target.detect { |r| id == r.id }
35 expects_array ? [ record ] : record
36 else
37 load_target.select { |r| ids.include?(r.id) }
38 end
39 else
40 conditions = "#{@finder_sql}"
41 if sanitized_conditions = sanitize_sql(options[:conditions])
42 conditions << " AND (#{sanitized_conditions})"
43 end
44
45 options[:conditions] = conditions
46
47 if options[:order] && @reflection.options[:order]
48 options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
49 elsif @reflection.options[:order]
50 options[:order] = @reflection.options[:order]
51 end
52
53 # Build options specific to association
54 construct_find_options!(options)
55
56 merge_options_from_reflection!(options)
57
58 # Pass through args exactly as we received them.
59 args << options
60 @reflection.klass.find(*args)
61 end
62 end
63
64 # Fetches the first one using SQL if possible.
65 def first(*args)
66 if fetch_first_or_last_using_find?(args)
67 find(:first, *args)
68 else
69 load_target unless loaded?
70 @target.first(*args)
71 end
72 end
73
74 # Fetches the last one using SQL if possible.
75 def last(*args)
76 if fetch_first_or_last_using_find?(args)
77 find(:last, *args)
78 else
79 load_target unless loaded?
80 @target.last(*args)
81 end
82 end
83
84 def to_ary
85 load_target
86 if @target.is_a?(Array)
87 @target.to_ary
88 else
89 Array(@target)
90 end
91 end
92
93 def reset
94 reset_target!
95 @loaded = false
96 end
97
98 def build(attributes = {}, &block)
99 if attributes.is_a?(Array)
100 attributes.collect { |attr| build(attr, &block) }
101 else
102 build_record(attributes) do |record|
103 block.call(record) if block_given?
104 set_belongs_to_association_for(record)
105 end
106 end
107 end
108
109 # Add +records+ to this association. Returns +self+ so method calls may be chained.
110 # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
111 def <<(*records)
112 result = true
113 load_target if @owner.new_record?
114
115 transaction do
116 flatten_deeper(records).each do |record|
117 raise_on_type_mismatch(record)
118 add_record_to_target_with_callbacks(record) do |r|
119 result &&= insert_record(record) unless @owner.new_record?
120 end
121 end
122 end
123
124 result && self
125 end
126
127 alias_method :push, :<<
128 alias_method :concat, :<<
129
130 # Starts a transaction in the association class's database connection.
131 #
132 # class Author < ActiveRecord::Base
133 # has_many :books
134 # end
135 #
136 # Author.find(:first).books.transaction do
137 # # same effect as calling Book.transaction
138 # end
139 def transaction(*args)
140 @reflection.klass.transaction(*args) do
141 yield
142 end
143 end
144
145 # Remove all records from this association
146 def delete_all
147 load_target
148 delete(@target)
149 reset_target!
150 end
151
152 # Calculate sum using SQL, not Enumerable
153 def sum(*args)
154 if block_given?
155 calculate(:sum, *args) { |*block_args| yield(*block_args) }
156 else
157 calculate(:sum, *args)
158 end
159 end
160
161 # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
162 # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
163 # descendant's +construct_sql+ method will have set :counter_sql automatically.
164 # Otherwise, construct options and pass them with scope to the target class's +count+.
165 def count(*args)
166 if @reflection.options[:counter_sql]
167 @reflection.klass.count_by_sql(@counter_sql)
168 else
169 column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
170 if @reflection.options[:uniq]
171 # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
172 column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
173 options.merge!(:distinct => true)
174 end
175
176 value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
177
178 limit = @reflection.options[:limit]
179 offset = @reflection.options[:offset]
180
181 if limit || offset
182 [ [value - offset.to_i, 0].max, limit.to_i ].min
183 else
184 value
185 end
186 end
187 end
188
189 # Removes +records+ from this association calling +before_remove+ and
190 # +after_remove+ callbacks.
191 #
192 # This method is abstract in the sense that +delete_records+ has to be
193 # provided by descendants. Note this method does not imply the records
194 # are actually removed from the database, that depends precisely on
195 # +delete_records+. They are in any case removed from the collection.
196 def delete(*records)
197 remove_records(records) do |records, old_records|
198 delete_records(old_records) if old_records.any?
199 records.each { |record| @target.delete(record) }
200 end
201 end
202
203 # Destroy +records+ and remove from this association calling +before_remove+
204 # and +after_remove+ callbacks.
205 #
206 # Note this method will always remove records from database ignoring the
207 # +:dependent+ option.
208 def destroy(*records)
209 remove_records(records) do |records, old_records|
210 old_records.each { |record| record.destroy }
211 end
212
213 load_target
214 end
215
216 # Removes all records from this association. Returns +self+ so method calls may be chained.
217 def clear
218 return self if length.zero? # forces load_target if it hasn't happened already
219
220 if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
221 destroy_all
222 else
223 delete_all
224 end
225
226 self
227 end
228
229 # Destory all the records from this association
230 def destroy_all
231 load_target
232 destroy(@target)
233 reset_target!
234 end
235
236 def create(attrs = {})
237 if attrs.is_a?(Array)
238 attrs.collect { |attr| create(attr) }
239 else
240 create_record(attrs) do |record|
241 yield(record) if block_given?
242 record.save
243 end
244 end
245 end
246
247 def create!(attrs = {})
248 create_record(attrs) do |record|
249 yield(record) if block_given?
250 record.save!
251 end
252 end
253
254 # Returns the size of the collection by executing a SELECT COUNT(*)
255 # query if the collection hasn't been loaded, and calling
256 # <tt>collection.size</tt> if it has.
257 #
258 # If the collection has been already loaded +size+ and +length+ are
259 # equivalent. If not and you are going to need the records anyway
260 # +length+ will take one less query. Otherwise +size+ is more efficient.
261 #
262 # This method is abstract in the sense that it relies on
263 # +count_records+, which is a method descendants have to provide.
264 def size
265 if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
266 @target.size
267 elsif !loaded? && @reflection.options[:group]
268 load_target.size
269 elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
270 unsaved_records = @target.select { |r| r.new_record? }
271 unsaved_records.size + count_records
272 else
273 count_records
274 end
275 end
276
277 # Returns the size of the collection calling +size+ on the target.
278 #
279 # If the collection has been already loaded +length+ and +size+ are
280 # equivalent. If not and you are going to need the records anyway this
281 # method will take one less query. Otherwise +size+ is more efficient.
282 def length
283 load_target.size
284 end
285
286 # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
287 # not been already loaded and you are going to fetch the records anyway
288 # it is better to check <tt>collection.length.zero?</tt>.
289 def empty?
290 size.zero?
291 end
292
293 def any?
294 if block_given?
295 method_missing(:any?) { |*block_args| yield(*block_args) }
296 else
297 !empty?
298 end
299 end
300
301 def uniq(collection = self)
302 seen = Set.new
303 collection.inject([]) do |kept, record|
304 unless seen.include?(record.id)
305 kept << record
306 seen << record.id
307 end
308 kept
309 end
310 end
311
312 # Replace this collection with +other_array+
313 # This will perform a diff and delete/add only records that have changed.
314 def replace(other_array)
315 other_array.each { |val| raise_on_type_mismatch(val) }
316
317 load_target
318 other = other_array.size < 100 ? other_array : other_array.to_set
319 current = @target.size < 100 ? @target : @target.to_set
320
321 transaction do
322 delete(@target.select { |v| !other.include?(v) })
323 concat(other_array.select { |v| !current.include?(v) })
324 end
325 end
326
327 def include?(record)
328 return false unless record.is_a?(@reflection.klass)
329 load_target if @reflection.options[:finder_sql] && !loaded?
330 return @target.include?(record) if loaded?
331 exists?(record)
332 end
333
334 def proxy_respond_to?(method, include_private = false)
335 super || @reflection.klass.respond_to?(method, include_private)
336 end
337
338 protected
339 def construct_find_options!(options)
340 end
341
342 def load_target
343 if !@owner.new_record? || foreign_key_present
344 begin
345 if !loaded?
346 if @target.is_a?(Array) && @target.any?
347 @target = find_target + @target.find_all {|t| t.new_record? }
348 else
349 @target = find_target
350 end
351 end
352 rescue ActiveRecord::RecordNotFound
353 reset
354 end
355 end
356
357 loaded if target
358 target
359 end
360
361 def method_missing(method, *args)
362 if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
363 if block_given?
364 super { |*block_args| yield(*block_args) }
365 else
366 super
367 end
368 elsif @reflection.klass.scopes.include?(method)
369 @reflection.klass.scopes[method].call(self, *args)
370 else
371 with_scope(construct_scope) do
372 if block_given?
373 @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
374 else
375 @reflection.klass.send(method, *args)
376 end
377 end
378 end
379 end
380
381 # overloaded in derived Association classes to provide useful scoping depending on association type.
382 def construct_scope
383 {}
384 end
385
386 def reset_target!
387 @target = Array.new
388 end
389
390 def find_target
391 records =
392 if @reflection.options[:finder_sql]
393 @reflection.klass.find_by_sql(@finder_sql)
394 else
395 find(:all)
396 end
397
398 @reflection.options[:uniq] ? uniq(records) : records
399 end
400
401 private
402
403 def create_record(attrs)
404 attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
405 ensure_owner_is_not_new
406 record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
407 @reflection.build_association(attrs)
408 end
409 if block_given?
410 add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
411 else
412 add_record_to_target_with_callbacks(record)
413 end
414 end
415
416 def build_record(attrs)
417 attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
418 record = @reflection.build_association(attrs)
419 if block_given?
420 add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
421 else
422 add_record_to_target_with_callbacks(record)
423 end
424 end
425
426 def add_record_to_target_with_callbacks(record)
427 callback(:before_add, record)
428 yield(record) if block_given?
429 @target ||= [] unless loaded?
430 @target << record unless @reflection.options[:uniq] && @target.include?(record)
431 callback(:after_add, record)
432 record
433 end
434
435 def remove_records(*records)
436 records = flatten_deeper(records)
437 records.each { |record| raise_on_type_mismatch(record) }
438
439 transaction do
440 records.each { |record| callback(:before_remove, record) }
441 old_records = records.reject { |r| r.new_record? }
442 yield(records, old_records)
443 records.each { |record| callback(:after_remove, record) }
444 end
445 end
446
447 def callback(method, record)
448 callbacks_for(method).each do |callback|
449 ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
450 end
451 end
452
453 def callbacks_for(callback_name)
454 full_callback_name = "#{callback_name}_for_#{@reflection.name}"
455 @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
456 end
457
458 def ensure_owner_is_not_new
459 if @owner.new_record?
460 raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
461 end
462 end
463
464 def fetch_first_or_last_using_find?(args)
465 args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
466 @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
467 end
468 end
469 end
470 end