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.
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.
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
)
25 options
= args
.extract_options
!
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
}
34 record
= load_target
.detect
{ |r
| id
== r
.id
}
35 expects_array
? [ record
] : record
37 load_target
.select
{ |r
| ids
.include?(r
.id
) }
40 conditions
= "#{@finder_sql}"
41 if sanitized_conditions
= sanitize_sql(options
[:conditions])
42 conditions
<< " AND (#{sanitized_conditions})"
45 options
[:conditions] = conditions
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]
53 # Build options specific to association
54 construct_find_options
!(options
)
56 merge_options_from_reflection
!(options
)
58 # Pass through args exactly as we received them.
60 @reflection.klass
.find(*args
)
64 # Fetches the first one using SQL if possible.
66 if fetch_first_or_last_using_find
?(args
)
69 load_target
unless loaded
?
74 # Fetches the last one using SQL if possible.
76 if fetch_first_or_last_using_find
?(args
)
79 load_target
unless loaded
?
86 if @target.is_a
?(Array
)
98 def build(attributes
= {}, &block
)
99 if attributes
.is_a
?(Array
)
100 attributes
.collect
{ |attr
| build(attr
, &block
) }
102 build_record(attributes
) do |record
|
103 block
.call(record
) if block_given
?
104 set_belongs_to_association_for(record
)
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.
113 load_target
if @owner.new_record
?
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
?
127 alias_method
:push, :<<
128 alias_method
:concat, :<<
130 # Starts a transaction in the association class's database connection.
132 # class Author < ActiveRecord::Base
136 # Author.find(:first).books.transaction do
137 # # same effect as calling Book.transaction
139 def transaction(*args
)
140 @reflection.klass
.transaction(*args
) do
145 # Remove all records from this association
152 # Calculate sum using SQL, not Enumerable
155 calculate(:sum, *args
) { |*block_args
| yield(*block_args
) }
157 calculate(:sum, *args
)
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+.
166 if @reflection.options
[:counter_sql]
167 @reflection.klass
.count_by_sql(@counter_sql)
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)
176 value
= @reflection.klass
.send(:with_scope, construct_scope
) { @reflection.klass
.count(column_name
, options
) }
178 limit
= @reflection.options
[:limit]
179 offset
= @reflection.options
[:offset]
182 [ [value
- offset
.to_i
, 0].max
, limit
.to_i
].min
189 # Removes +records+ from this association calling +before_remove+ and
190 # +after_remove+ callbacks.
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.
197 remove_records(records
) do |records
, old_records
|
198 delete_records(old_records
) if old_records
.any
?
199 records
.each
{ |record
| @target.delete(record
) }
203 # Destroy +records+ and remove from this association calling +before_remove+
204 # and +after_remove+ callbacks.
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
}
216 # Removes all records from this association. Returns +self+ so method calls may be chained.
218 return self if length
.zero
? # forces load_target if it hasn't happened already
220 if @reflection.options
[:dependent] && @reflection.options
[:dependent] == :destroy
229 # Destory all the records from this association
236 def create(attrs
= {})
237 if attrs
.is_a
?(Array
)
238 attrs
.collect
{ |attr
| create(attr
) }
240 create_record(attrs
) do |record
|
241 yield(record
) if block_given
?
247 def create
!(attrs
= {})
248 create_record(attrs
) do |record
|
249 yield(record
) if block_given
?
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.
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.
262 # This method is abstract in the sense that it relies on
263 # +count_records+, which is a method descendants have to provide.
265 if @owner.new_record
? || (loaded
? && !@reflection.options
[:uniq])
267 elsif !loaded
? && @reflection.options
[:group]
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
277 # Returns the size of the collection calling +size+ on the target.
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.
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>.
295 method_missing(:any?) { |*block_args
| yield(*block_args
) }
301 def uniq(collection
= self)
303 collection
.inject([]) do |kept
, record
|
304 unless seen
.include?(record
.id
)
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
) }
318 other
= other_array
.size
< 100 ? other_array
: other_array
.to_set
319 current
= @target.size
< 100 ? @target : @target.to_set
322 delete(@target.select
{ |v
| !other
.include?(v
) })
323 concat(other_array
.select
{ |v
| !current
.include?(v
) })
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
?
334 def proxy_respond_to
?(method
, include_private
= false)
335 super || @reflection.klass
.respond_to
?(method
, include_private
)
339 def construct_find_options
!(options
)
343 if !@owner.new_record
? || foreign_key_present
346 if @target.is_a
?(Array
) && @target.any
?
347 @target = find_target
+ @target.find_all
{|t
| t
.new_record
? }
349 @target = find_target
352 rescue ActiveRecord
::RecordNotFound
361 def method_missing(method
, *args
)
362 if @target.respond_to
?(method
) || (!@reflection.klass
.respond_to
?(method
) && Class
.respond_to
?(method
))
364 super { |*block_args
| yield(*block_args
) }
368 elsif @reflection.klass
.scopes
.include?(method
)
369 @reflection.klass
.scopes
[method
].call(self, *args
)
371 with_scope(construct_scope
) do
373 @reflection.klass
.send(method
, *args
) { |*block_args
| yield(*block_args
) }
375 @reflection.klass
.send(method
, *args
)
381 # overloaded in derived Association classes to provide useful scoping depending on association type.
392 if @reflection.options
[:finder_sql]
393 @reflection.klass
.find_by_sql(@finder_sql)
398 @reflection.options
[:uniq] ? uniq(records
) : records
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
)
410 add_record_to_target_with_callbacks(record
) { |*block_args
| yield(*block_args
) }
412 add_record_to_target_with_callbacks(record
)
416 def build_record(attrs
)
417 attrs
.update(@reflection.options
[:conditions]) if @reflection.options
[:conditions].is_a
?(Hash
)
418 record
= @reflection.build_association(attrs
)
420 add_record_to_target_with_callbacks(record
) { |*block_args
| yield(*block_args
) }
422 add_record_to_target_with_callbacks(record
)
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
)
435 def remove_records(*records
)
436 records
= flatten_deeper(records
)
437 records
.each
{ |record
| raise_on_type_mismatch(record
) }
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
) }
447 def callback(method
, record
)
448 callbacks_for(method
).each
do |callback
|
449 ActiveSupport
::Callbacks::Callback.new(method
, callback
, record
).call(@owner, record
)
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
) || []
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"
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
))