diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/has_many_association.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/has_many_association.rb | 129 |
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 |