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
?
94 def build(attributes
= {}, &block
)
95 if attributes
.is_a
?(Array
)
96 attributes
.collect
{ |attr
| build(attr
, &block
) }
98 build_record(attributes
) do |record
|
99 block
.call(record
) if block_given
?
100 set_belongs_to_association_for(record
)
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.
109 load_target
if @owner.new_record
?
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
?
123 alias_method
:push, :<<
124 alias_method
:concat, :<<
126 # Starts a transaction in the association class's database connection.
128 # class Author < ActiveRecord::Base
132 # Author.find(:first).books.transaction do
133 # # same effect as calling Book.transaction
135 def transaction(*args
)
136 @reflection.klass
.transaction(*args
) do
141 # Remove all records from this association
148 # Calculate sum using SQL, not Enumerable
151 calculate(:sum, *args
) { |*block_args
| yield(*block_args
) }
153 calculate(:sum, *args
)
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+.
162 if @reflection.options
[:counter_sql]
163 @reflection.klass
.count_by_sql(@counter_sql)
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)
172 value
= @reflection.klass
.send(:with_scope, construct_scope
) { @reflection.klass
.count(column_name
, options
) }
174 limit
= @reflection.options
[:limit]
175 offset
= @reflection.options
[:offset]
178 [ [value
- offset
.to_i
, 0].max
, limit
.to_i
].min
186 # Removes +records+ from this association calling +before_remove+ and
187 # +after_remove+ callbacks.
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.
194 records
= flatten_deeper(records
)
195 records
.each
{ |record
| raise_on_type_mismatch(record
) }
198 records
.each
{ |record
| callback(:before_remove, record
) }
200 old_records
= records
.reject
{|r
| r
.new_record
? }
201 delete_records(old_records
) if old_records
.any
?
203 records
.each
do |record
|
204 @target.delete(record
)
205 callback(:after_remove, record
)
210 # Removes all records from this association. Returns +self+ so method calls may be chained.
212 return self if length
.zero
? # forces load_target if it hasn't happened already
214 if @reflection.options
[:dependent] && @reflection.options
[:dependent] == :destroy
225 each
{ |record
| record
.destroy
}
231 def create(attrs
= {})
232 if attrs
.is_a
?(Array
)
233 attrs
.collect
{ |attr
| create(attr
) }
235 create_record(attrs
) do |record
|
236 yield(record
) if block_given
?
242 def create
!(attrs
= {})
243 create_record(attrs
) do |record
|
244 yield(record
) if block_given
?
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.
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.
257 # This method is abstract in the sense that it relies on
258 # +count_records+, which is a method descendants have to provide.
260 if @owner.new_record
? || (loaded
? && !@reflection.options
[:uniq])
262 elsif !loaded
? && @reflection.options
[:group]
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
272 # Returns the size of the collection calling +size+ on the target.
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.
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>.
290 method_missing(:any?) { |*block_args
| yield(*block_args
) }
296 def uniq(collection
= self)
298 collection
.inject([]) do |kept
, record
|
299 unless seen
.include?(record
.id
)
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
) }
313 other
= other_array
.size
< 100 ? other_array
: other_array
.to_set
314 current
= @target.size
< 100 ? @target : @target.to_set
317 delete(@target.select
{ |v
| !other
.include?(v
) })
318 concat(other_array
.select
{ |v
| !current
.include?(v
) })
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
?
329 def proxy_respond_to
?(method
, include_private
= false)
330 super || @reflection.klass
.respond_to
?(method
, include_private
)
334 def construct_find_options
!(options
)
338 if !@owner.new_record
? || foreign_key_present
341 if @target.is_a
?(Array
) && @target.any
?
342 @target = find_target
+ @target.find_all
{|t
| t
.new_record
? }
344 @target = find_target
347 rescue ActiveRecord
::RecordNotFound
356 def method_missing(method
, *args
)
357 if @target.respond_to
?(method
) || (!@reflection.klass
.respond_to
?(method
) && Class
.respond_to
?(method
))
359 super { |*block_args
| yield(*block_args
) }
363 elsif @reflection.klass
.scopes
.include?(method
)
364 @reflection.klass
.scopes
[method
].call(self, *args
)
366 with_scope(construct_scope
) do
368 @reflection.klass
.send(method
, *args
) { |*block_args
| yield(*block_args
) }
370 @reflection.klass
.send(method
, *args
)
376 # overloaded in derived Association classes to provide useful scoping depending on association type.
387 if @reflection.options
[:finder_sql]
388 @reflection.klass
.find_by_sql(@finder_sql)
393 @reflection.options
[:uniq] ? uniq(records
) : records
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
)
405 add_record_to_target_with_callbacks(record
) { |*block_args
| yield(*block_args
) }
407 add_record_to_target_with_callbacks(record
)
411 def build_record(attrs
)
412 attrs
.update(@reflection.options
[:conditions]) if @reflection.options
[:conditions].is_a
?(Hash
)
413 record
= @reflection.build_association(attrs
)
415 add_record_to_target_with_callbacks(record
) { |*block_args
| yield(*block_args
) }
417 add_record_to_target_with_callbacks(record
)
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
)
430 def callback(method
, record
)
431 callbacks_for(method
).each
do |callback
|
432 ActiveSupport
::Callbacks::Callback.new(method
, callback
, record
).call(@owner, record
)
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
) || []
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"
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
))