Froze rails gems
[depot.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 @target.to_ary
87 end
88
89 def reset
90 reset_target!
91 @loaded = false
92 end
93
94 def build(attributes = {}, &block)
95 if attributes.is_a?(Array)
96 attributes.collect { |attr| build(attr, &block) }
97 else
98 build_record(attributes) do |record|
99 block.call(record) if block_given?
100 set_belongs_to_association_for(record)
101 end
102 end
103 end
104
105 # Add +records+ to this association. Returns +self+ so method calls may be chained.
106 # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
107 def <<(*records)
108 result = true
109 load_target if @owner.new_record?
110
111 transaction do
112 flatten_deeper(records).each do |record|
113 raise_on_type_mismatch(record)
114 add_record_to_target_with_callbacks(record) do |r|
115 result &&= insert_record(record) unless @owner.new_record?
116 end
117 end
118 end
119
120 result && self
121 end
122
123 alias_method :push, :<<
124 alias_method :concat, :<<
125
126 # Starts a transaction in the association class's database connection.
127 #
128 # class Author < ActiveRecord::Base
129 # has_many :books
130 # end
131 #
132 # Author.find(:first).books.transaction do
133 # # same effect as calling Book.transaction
134 # end
135 def transaction(*args)
136 @reflection.klass.transaction(*args) do
137 yield
138 end
139 end
140
141 # Remove all records from this association
142 def delete_all
143 load_target
144 delete(@target)
145 reset_target!
146 end
147
148 # Calculate sum using SQL, not Enumerable
149 def sum(*args)
150 if block_given?
151 calculate(:sum, *args) { |*block_args| yield(*block_args) }
152 else
153 calculate(:sum, *args)
154 end
155 end
156
157 # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
158 # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
159 # descendant's +construct_sql+ method will have set :counter_sql automatically.
160 # Otherwise, construct options and pass them with scope to the target class's +count+.
161 def count(*args)
162 if @reflection.options[:counter_sql]
163 @reflection.klass.count_by_sql(@counter_sql)
164 else
165 column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
166 if @reflection.options[:uniq]
167 # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
168 column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
169 options.merge!(:distinct => true)
170 end
171
172 value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
173
174 limit = @reflection.options[:limit]
175 offset = @reflection.options[:offset]
176
177 if limit || offset
178 [ [value - offset.to_i, 0].max, limit.to_i ].min
179 else
180 value
181 end
182 end
183 end
184
185
186 # Removes +records+ from this association calling +before_remove+ and
187 # +after_remove+ callbacks.
188 #
189 # This method is abstract in the sense that +delete_records+ has to be
190 # provided by descendants. Note this method does not imply the records
191 # are actually removed from the database, that depends precisely on
192 # +delete_records+. They are in any case removed from the collection.
193 def delete(*records)
194 records = flatten_deeper(records)
195 records.each { |record| raise_on_type_mismatch(record) }
196
197 transaction do
198 records.each { |record| callback(:before_remove, record) }
199
200 old_records = records.reject {|r| r.new_record? }
201 delete_records(old_records) if old_records.any?
202
203 records.each do |record|
204 @target.delete(record)
205 callback(:after_remove, record)
206 end
207 end
208 end
209
210 # Removes all records from this association. Returns +self+ so method calls may be chained.
211 def clear
212 return self if length.zero? # forces load_target if it hasn't happened already
213
214 if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
215 destroy_all
216 else
217 delete_all
218 end
219
220 self
221 end
222
223 def destroy_all
224 transaction do
225 each { |record| record.destroy }
226 end
227
228 reset_target!
229 end
230
231 def create(attrs = {})
232 if attrs.is_a?(Array)
233 attrs.collect { |attr| create(attr) }
234 else
235 create_record(attrs) do |record|
236 yield(record) if block_given?
237 record.save
238 end
239 end
240 end
241
242 def create!(attrs = {})
243 create_record(attrs) do |record|
244 yield(record) if block_given?
245 record.save!
246 end
247 end
248
249 # Returns the size of the collection by executing a SELECT COUNT(*)
250 # query if the collection hasn't been loaded, and calling
251 # <tt>collection.size</tt> if it has.
252 #
253 # If the collection has been already loaded +size+ and +length+ are
254 # equivalent. If not and you are going to need the records anyway
255 # +length+ will take one less query. Otherwise +size+ is more efficient.
256 #
257 # This method is abstract in the sense that it relies on
258 # +count_records+, which is a method descendants have to provide.
259 def size
260 if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
261 @target.size
262 elsif !loaded? && @reflection.options[:group]
263 load_target.size
264 elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
265 unsaved_records = @target.select { |r| r.new_record? }
266 unsaved_records.size + count_records
267 else
268 count_records
269 end
270 end
271
272 # Returns the size of the collection calling +size+ on the target.
273 #
274 # If the collection has been already loaded +length+ and +size+ are
275 # equivalent. If not and you are going to need the records anyway this
276 # method will take one less query. Otherwise +size+ is more efficient.
277 def length
278 load_target.size
279 end
280
281 # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
282 # not been already loaded and you are going to fetch the records anyway
283 # it is better to check <tt>collection.length.zero?</tt>.
284 def empty?
285 size.zero?
286 end
287
288 def any?
289 if block_given?
290 method_missing(:any?) { |*block_args| yield(*block_args) }
291 else
292 !empty?
293 end
294 end
295
296 def uniq(collection = self)
297 seen = Set.new
298 collection.inject([]) do |kept, record|
299 unless seen.include?(record.id)
300 kept << record
301 seen << record.id
302 end
303 kept
304 end
305 end
306
307 # Replace this collection with +other_array+
308 # This will perform a diff and delete/add only records that have changed.
309 def replace(other_array)
310 other_array.each { |val| raise_on_type_mismatch(val) }
311
312 load_target
313 other = other_array.size < 100 ? other_array : other_array.to_set
314 current = @target.size < 100 ? @target : @target.to_set
315
316 transaction do
317 delete(@target.select { |v| !other.include?(v) })
318 concat(other_array.select { |v| !current.include?(v) })
319 end
320 end
321
322 def include?(record)
323 return false unless record.is_a?(@reflection.klass)
324 load_target if @reflection.options[:finder_sql] && !loaded?
325 return @target.include?(record) if loaded?
326 exists?(record)
327 end
328
329 def proxy_respond_to?(method, include_private = false)
330 super || @reflection.klass.respond_to?(method, include_private)
331 end
332
333 protected
334 def construct_find_options!(options)
335 end
336
337 def load_target
338 if !@owner.new_record? || foreign_key_present
339 begin
340 if !loaded?
341 if @target.is_a?(Array) && @target.any?
342 @target = find_target + @target.find_all {|t| t.new_record? }
343 else
344 @target = find_target
345 end
346 end
347 rescue ActiveRecord::RecordNotFound
348 reset
349 end
350 end
351
352 loaded if target
353 target
354 end
355
356 def method_missing(method, *args)
357 if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
358 if block_given?
359 super { |*block_args| yield(*block_args) }
360 else
361 super
362 end
363 elsif @reflection.klass.scopes.include?(method)
364 @reflection.klass.scopes[method].call(self, *args)
365 else
366 with_scope(construct_scope) do
367 if block_given?
368 @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
369 else
370 @reflection.klass.send(method, *args)
371 end
372 end
373 end
374 end
375
376 # overloaded in derived Association classes to provide useful scoping depending on association type.
377 def construct_scope
378 {}
379 end
380
381 def reset_target!
382 @target = Array.new
383 end
384
385 def find_target
386 records =
387 if @reflection.options[:finder_sql]
388 @reflection.klass.find_by_sql(@finder_sql)
389 else
390 find(:all)
391 end
392
393 @reflection.options[:uniq] ? uniq(records) : records
394 end
395
396 private
397
398 def create_record(attrs)
399 attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
400 ensure_owner_is_not_new
401 record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
402 @reflection.build_association(attrs)
403 end
404 if block_given?
405 add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
406 else
407 add_record_to_target_with_callbacks(record)
408 end
409 end
410
411 def build_record(attrs)
412 attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
413 record = @reflection.build_association(attrs)
414 if block_given?
415 add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
416 else
417 add_record_to_target_with_callbacks(record)
418 end
419 end
420
421 def add_record_to_target_with_callbacks(record)
422 callback(:before_add, record)
423 yield(record) if block_given?
424 @target ||= [] unless loaded?
425 @target << record unless @reflection.options[:uniq] && @target.include?(record)
426 callback(:after_add, record)
427 record
428 end
429
430 def callback(method, record)
431 callbacks_for(method).each do |callback|
432 ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
433 end
434 end
435
436 def callbacks_for(callback_name)
437 full_callback_name = "#{callback_name}_for_#{@reflection.name}"
438 @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
439 end
440
441 def ensure_owner_is_not_new
442 if @owner.new_record?
443 raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
444 end
445 end
446
447 def fetch_first_or_last_using_find?(args)
448 args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
449 @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
450 end
451 end
452 end
453 end