aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/has_many_association.rb
blob: 4e0c89791e0b17ab173c7238876814ab778e7f89 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# frozen_string_literal: true
module ActiveRecord
  # = Active Record Has Many Association
  module Associations
    # 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.
    class HasManyAssociation < CollectionAssociation #:nodoc:
      include ForeignAssociation

      def handle_dependency
        case options[:dependent]
        when :restrict_with_exception
          raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?

        when :restrict_with_error
          unless empty?
            record = owner.class.human_attribute_name(reflection.name).downcase
            owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record)
            throw(:abort)
          end

        when :destroy
          # No point in executing the counter update since we're going to destroy the parent anyway
          load_target.each { |t| t.destroyed_by_association = reflection }
          destroy_all
        else
          delete_all
        end
      end

      def insert_record(record, validate = true, raise = false)
        set_owner_attributes(record)
        super
      end

      def empty?
        if reflection.has_cached_counter?
          size.zero?
        else
          super
        end
      end

      private

        # Returns the number of records in this collection.
        #
        # If the association has a counter cache it gets that value. Otherwise
        # 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
        # to do an SQL count, in those cases the array count will be used.
        #
        # 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.
        def count_records
          count = if reflection.has_cached_counter?
            owner._read_attribute(reflection.counter_cache_column).to_i
          else
            scope.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 ||= []) && loaded! if count == 0

          [association_scope.limit_value, count].compact.min
        end

        def update_counter(difference, reflection = reflection())
          if reflection.has_cached_counter?
            owner.increment!(reflection.counter_cache_column, difference)
          end
        end

        def update_counter_in_memory(difference, reflection = reflection())
          if reflection.counter_must_be_updated_by_has_many?
            counter = reflection.counter_cache_column
            owner.increment(counter, difference)
            owner.send(:clear_attribute_change, counter) # eww
          end
        end

        def delete_count(method, scope)
          if method == :delete_all
            scope.delete_all
          else
            scope.update_all(reflection.foreign_key => nil)
          end
        end

        def delete_or_nullify_all_records(method)
          count = delete_count(method, scope)
          update_counter(-count)
        end

        # Deletes the records according to the <tt>:dependent</tt> option.
        def delete_records(records, method)
          if method == :destroy
            records.each(&:destroy!)
            update_counter(-records.length) unless reflection.inverse_updates_counter_cache?
          else
            scope = self.scope.where(reflection.klass.primary_key => records)
            update_counter(-delete_count(method, scope))
          end
        end

        def concat_records(records, *)
          update_counter_if_success(super, records.length)
        end

        def _create_record(attributes, *)
          if attributes.is_a?(Array)
            super
          else
            update_counter_if_success(super, 1)
          end
        end

        def update_counter_if_success(saved_successfully, difference)
          if saved_successfully
            update_counter_in_memory(difference)
          end
          saved_successfully
        end
    end
  end
end