aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/has_many_association.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/has_many_association.rb')
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb129
1 files changed, 50 insertions, 79 deletions
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index c33bc6aa47..78c5c4b870 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -5,19 +5,14 @@ module ActiveRecord
#
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
- class HasManyAssociation < AssociationCollection #:nodoc:
- def initialize(owner, reflection)
- @finder_sql = nil
- super
+ class HasManyAssociation < CollectionAssociation #:nodoc:
+
+ def insert_record(record, validate = true)
+ set_owner_attributes(record)
+ record.save(:validate => validate)
end
- protected
- def owner_quoted_id
- if @reflection.options[:primary_key]
- quote_value(@owner.send(@reflection.options[:primary_key]))
- else
- @owner.quoted_id
- end
- end
+
+ private
# Returns the number of records in this collection.
#
@@ -34,94 +29,70 @@ module ActiveRecord
# the loaded flag is set to true as well.
def count_records
count = if has_cached_counter?
- @owner.send(:read_attribute, cached_counter_attribute_name)
- elsif @reflection.options[:counter_sql]
- @reflection.klass.count_by_sql(@counter_sql)
+ owner.send(:read_attribute, cached_counter_attribute_name)
+ elsif options[:counter_sql] || options[:finder_sql]
+ reflection.klass.count_by_sql(custom_counter_sql)
else
- @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
+ scoped.count
end
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
- @target ||= [] and loaded if count == 0
-
- if @reflection.options[:limit]
- count = [ @reflection.options[:limit], count ].min
- end
-
- return count
- end
+ @target ||= [] and loaded! if count == 0
- def has_cached_counter?
- @owner.attribute_present?(cached_counter_attribute_name)
+ [options[:limit], count].compact.min
end
- def cached_counter_attribute_name
- "#{@reflection.name}_count"
+ def has_cached_counter?(reflection = reflection)
+ owner.attribute_present?(cached_counter_attribute_name(reflection))
end
- def insert_record(record, force = false, validate = true)
- set_belongs_to_association_for(record)
- force ? record.save! : record.save(:validate => validate)
+ def cached_counter_attribute_name(reflection = reflection)
+ "#{reflection.name}_count"
end
- # Deletes the records according to the <tt>:dependent</tt> option.
- def delete_records(records)
- case @reflection.options[:dependent]
- when :destroy
- records.each { |r| r.destroy }
- when :delete_all
- @reflection.klass.delete(records.map { |record| record.id })
- else
- relation = Arel::Table.new(@reflection.table_name)
- relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
- and(Arel::Predicates::In.new(relation[@reflection.klass.primary_key], records.map(&:id)))
- ).update(relation[@reflection.primary_key_name] => nil)
-
- @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
+ def update_counter(difference, reflection = reflection)
+ if has_cached_counter?(reflection)
+ counter = cached_counter_attribute_name(reflection)
+ owner.class.update_counters(owner.id, counter => difference)
+ owner[counter] += difference
+ owner.changed_attributes.delete(counter) # eww
end
end
- def target_obsolete?
- false
+ # This shit is nasty. We need to avoid the following situation:
+ #
+ # * An associated record is deleted via record.destroy
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
+ # :counter_cache options which points back at our owner. So they update the
+ # counter cache.
+ # * In which case, we must make sure to *not* update the counter cache, or else
+ # it will be decremented twice.
+ #
+ # Hence this method.
+ def inverse_updates_counter_cache?(reflection = reflection)
+ counter_name = cached_counter_attribute_name(reflection)
+ reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
+ inverse_reflection.counter_cache_column == counter_name
+ }
end
- def construct_sql
- case
- when @reflection.options[:finder_sql]
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
-
- when @reflection.options[:as]
- @finder_sql =
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
- @finder_sql << " AND (#{conditions})" if conditions
+ # Deletes the records according to the <tt>:dependent</tt> option.
+ def delete_records(records, method)
+ if method == :destroy
+ records.each { |r| r.destroy }
+ update_counter(-records.length) unless inverse_updates_counter_cache?
+ else
+ keys = records.map { |r| r[reflection.association_primary_key] }
+ scope = scoped.where(reflection.association_primary_key => keys)
+ if method == :delete_all
+ update_counter(-scope.delete_all)
else
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- @finder_sql << " AND (#{conditions})" if conditions
+ update_counter(-scope.update_all(reflection.foreign_key => nil))
+ end
end
-
- construct_counter_sql
- end
-
- def construct_scope
- create_scoping = {}
- set_belongs_to_association_for(create_scoping)
- {
- :find => { :conditions => @finder_sql,
- :readonly => false,
- :order => @reflection.options[:order],
- :limit => @reflection.options[:limit],
- :include => @reflection.options[:include]},
- :create => create_scoping
- }
- end
-
- def we_can_set_the_inverse_on_this?(record)
- inverse = @reflection.inverse_of
- return !inverse.nil?
end
end
end