has_many_association.rb 4.2 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1
module ActiveRecord
2
  # = Active Record Has Many Association
D
Initial  
David Heinemeier Hansson 已提交
3
  module Associations
P
Pratik Naik 已提交
4 5 6 7
    # This is the proxy that handles a has many association.
    #
    # If the association has a <tt>:through</tt> option further specialization
    # is provided by its child HasManyThroughAssociation.
D
Initial  
David Heinemeier Hansson 已提交
8 9
    class HasManyAssociation < AssociationCollection #:nodoc:
      protected
10 11 12 13 14 15 16 17

        def insert_record(record, force = false, validate = true)
          set_owner_attributes(record)
          save_record(record, force, validate)
        end

      private

P
Pratik Naik 已提交
18 19 20
        # Returns the number of records in this collection.
        #
        # If the association has a counter cache it gets that value. Otherwise
21 22
        # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
        # there's one.  Some configuration options like :group make it impossible
23
        # to do an SQL count, in those cases the array count will be used.
24
        #
P
Pratik Naik 已提交
25 26 27 28 29 30
        # That does not depend on whether the collection has already been loaded
        # or not. The +size+ method is the one that takes the loaded flag into
        # account and delegates to +count_records+ if needed.
        #
        # If the collection is empty the target is set to an empty array and
        # the loaded flag is set to true as well.
D
Initial  
David Heinemeier Hansson 已提交
31
        def count_records
32
          count = if has_cached_counter?
D
Initial  
David Heinemeier Hansson 已提交
33
            @owner.send(:read_attribute, cached_counter_attribute_name)
34 35
          elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql]
            @reflection.klass.count_by_sql(custom_counter_sql)
D
Initial  
David Heinemeier Hansson 已提交
36
          else
37
            scoped.count
D
Initial  
David Heinemeier Hansson 已提交
38
          end
39 40 41 42

          # 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.
43
          @target ||= [] and loaded! if count == 0
44

A
Aaron Patterson 已提交
45
          [@reflection.options[:limit], count].compact.min
D
Initial  
David Heinemeier Hansson 已提交
46
        end
47

48 49
        def has_cached_counter?(reflection = @reflection)
          @owner.attribute_present?(cached_counter_attribute_name(reflection))
D
Initial  
David Heinemeier Hansson 已提交
50
        end
51

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
        def cached_counter_attribute_name(reflection = @reflection)
          "#{reflection.name}_count"
        end

        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

        # 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
          }
D
Initial  
David Heinemeier Hansson 已提交
80 81
        end

82
        # Deletes the records according to the <tt>:dependent</tt> option.
83
        def delete_records(records, method = @reflection.options[:dependent])
84
          if method == :destroy
85
            records.each { |r| r.destroy }
86
            update_counter(-records.length) unless inverse_updates_counter_cache?
87
          else
88 89
            keys  = records.map { |r| r[@reflection.association_primary_key] }
            scope = scoped.where(@reflection.association_primary_key => keys)
90

91 92 93 94 95
            if method == :delete_all
              update_counter(-scope.delete_all)
            else
              update_counter(-scope.update_all(@reflection.foreign_key => nil))
            end
96
          end
D
Initial  
David Heinemeier Hansson 已提交
97
        end
98

99
        alias creation_attributes construct_owner_attributes
D
Initial  
David Heinemeier Hansson 已提交
100 101 102
    end
  end
end