has_many_association.rb 4.1 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.
8 9 10 11 12
    class HasManyAssociation < CollectionAssociation #:nodoc:
      def insert_record(record, validate = true)
        set_owner_attributes(record)
        record.save(:validate => validate)
      end
13 14 15

      private

P
Pratik Naik 已提交
16 17 18
        # Returns the number of records in this collection.
        #
        # If the association has a counter cache it gets that value. Otherwise
19 20
        # 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
21
        # to do an SQL count, in those cases the array count will be used.
22
        #
P
Pratik Naik 已提交
23 24 25 26 27 28
        # 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 已提交
29
        def count_records
30
          count = if has_cached_counter?
31 32 33
            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)
D
Initial  
David Heinemeier Hansson 已提交
34
          else
35
            scoped.count
D
Initial  
David Heinemeier Hansson 已提交
36
          end
37 38 39 40

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

43
          [reflection.options[:limit], count].compact.min
D
Initial  
David Heinemeier Hansson 已提交
44
        end
45

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

50
        def cached_counter_attribute_name(reflection = reflection)
51 52 53
          "#{reflection.name}_count"
        end

54
        def update_counter(difference, reflection = reflection)
55 56
          if has_cached_counter?(reflection)
            counter = cached_counter_attribute_name(reflection)
57 58 59
            owner.class.update_counters(owner.id, counter => difference)
            owner[counter] += difference
            owner.changed_attributes.delete(counter) # eww
60 61 62 63 64 65 66
          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
67
        #     :counter_cache options which points back at our owner. So they update the
68 69 70 71 72
        #     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.
73
        def inverse_updates_counter_cache?(reflection = reflection)
74 75 76 77
          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 已提交
78 79
        end

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

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

97
        alias creation_attributes construct_owner_attributes
D
Initial  
David Heinemeier Hansson 已提交
98 99 100
    end
  end
end