diff options
author | Jon Leighton <j@jonathanleighton.com> | 2011-03-02 21:24:56 +0000 |
---|---|---|
committer | Jon Leighton <j@jonathanleighton.com> | 2011-03-04 09:30:27 +0000 |
commit | 735844db712c511dd8abf36a5279318fbc0ff9d0 (patch) | |
tree | 5fbd5d224ef85d8c878bf221db98b422c9345466 /activerecord/lib/active_record/associations/has_many_association.rb | |
parent | 9a98c766e045aebc2ef6d5b716936b73407f095d (diff) | |
parent | b171b9e73dcc6a89b1da652da61c5127fe605b51 (diff) | |
download | rails-735844db712c511dd8abf36a5279318fbc0ff9d0.tar.gz rails-735844db712c511dd8abf36a5279318fbc0ff9d0.tar.bz2 rails-735844db712c511dd8abf36a5279318fbc0ff9d0.zip |
Merge branch 'master' into nested_has_many_through
Conflicts:
activerecord/CHANGELOG
activerecord/lib/active_record/association_preload.rb
activerecord/lib/active_record/associations.rb
activerecord/lib/active_record/associations/class_methods/join_dependency.rb
activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb
activerecord/lib/active_record/associations/has_many_association.rb
activerecord/lib/active_record/associations/has_many_through_association.rb
activerecord/lib/active_record/associations/has_one_association.rb
activerecord/lib/active_record/associations/has_one_through_association.rb
activerecord/lib/active_record/associations/through_association_scope.rb
activerecord/lib/active_record/reflection.rb
activerecord/test/cases/associations/has_many_through_associations_test.rb
activerecord/test/cases/associations/has_one_through_associations_test.rb
activerecord/test/cases/reflection_test.rb
activerecord/test/cases/relations_test.rb
activerecord/test/fixtures/memberships.yml
activerecord/test/models/categorization.rb
activerecord/test/models/category.rb
activerecord/test/models/member.rb
activerecord/test/models/reference.rb
activerecord/test/models/tagging.rb
Diffstat (limited to 'activerecord/lib/active_record/associations/has_many_association.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/has_many_association.rb | 121 |
1 files changed, 53 insertions, 68 deletions
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 851b10e300..cebf3e477a 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -5,15 +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: - protected - def owner_quoted_id(reflection = @reflection) - if reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(reflection.options[:primary_key])) - else - @owner.quoted_id - end - end + class HasManyAssociation < CollectionAssociation #:nodoc: + + def insert_record(record, validate = true) + set_owner_attributes(record) + record.save(:validate => validate) + end + + private # Returns the number of records in this collection. # @@ -30,87 +29,73 @@ 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.options[:finder_sql] - @reflection.klass.count_by_sql(custom_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(@scope[:find].slice(:conditions, :joins, :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 + @target ||= [] and loaded! if count == 0 - [@reflection.options[:limit], count].compact.min + [options[:limit], count].compact.min end - def has_cached_counter? - @owner.attribute_present?(cached_counter_attribute_name) + def has_cached_counter?(reflection = reflection) + owner.attribute_present?(cached_counter_attribute_name(reflection)) end - def cached_counter_attribute_name - "#{@reflection.name}_count" + def cached_counter_attribute_name(reflection = reflection) + "#{reflection.name}_count" end - def insert_record(record, force = false, validate = true) - set_belongs_to_association_for(record) - force ? record.save! : record.save(:validate => validate) - 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) - stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id). - and(relation[@reflection.klass.primary_key].in(records.map { |r| r.id })) - ).compile_update(relation[@reflection.primary_key_name] => nil) - @owner.connection.update stmt.to_sql - - @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_conditions - if @reflection.options[:as] - 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)}" + # 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 - sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - end - sql << " AND (#{conditions})" if conditions - sql - end + keys = records.map { |r| r[reflection.association_primary_key] } + scope = scoped.where(reflection.association_primary_key => keys) - def construct_find_scope - { - :conditions => construct_conditions, - :readonly => false, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :include => @reflection.options[:include] - } - end - - def construct_create_scope - create_scoping = {} - set_belongs_to_association_for(create_scoping) - create_scoping + if method == :delete_all + update_counter(-scope.delete_all) + else + update_counter(-scope.update_all(reflection.foreign_key => nil)) + end + end end - def we_can_set_the_inverse_on_this?(record) - @reflection.inverse_of - end + alias creation_attributes construct_owner_attributes end end end |