aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/counter_cache.rb
blob: 0ba54e6248dfdf5f81fdb4979bee58186259c060 (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
module ActiveRecord
  # = Active Record Counter Cache
  module CounterCache
    # Resets one or more counter caches to their correct value using an SQL
    # count query. This is useful when adding new counter caches, or if the
    # counter has been corrupted or modified directly by SQL.
    #
    # ==== Parameters
    #
    # * +id+ - The id of the object you wish to reset a counter on.
    # * +counters+ - One or more counter names to reset
    #
    # ==== Examples
    #
    #   # For Post with id #1 records reset the comments_count
    #   Post.reset_counters(1, :comments)
    def reset_counters(id, *counters)
      object = find(id)
      counters.each do |association|
        has_many_association = reflect_on_association(association.to_sym)

        if has_many_association.options[:as]
          has_many_association.options[:as].to_s.classify
        else
          self.name
        end

        foreign_key  = has_many_association.foreign_key.to_s
        child_class  = has_many_association.klass
        belongs_to   = child_class.reflect_on_all_associations(:belongs_to)
        reflection   = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
        counter_name = reflection.counter_cache_column

        stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
          arel_table[counter_name] => object.send(association).count
        })
        connection.update stmt
      end
      return true
    end

    # A generic "counter updater" implementation, intended primarily to be
    # used by increment_counter and decrement_counter, but which may also
    # be useful on its own. It simply does a direct SQL update for the record
    # with the given ID, altering the given hash of counters by the amount
    # given by the corresponding value:
    #
    # ==== Parameters
    #
    # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
    # * +counters+ - An Array of Hashes containing the names of the fields
    #   to update as keys and the amount to update the field by as values.
    #
    # ==== Examples
    #
    #   # For the Post with id of 5, decrement the comment_count by 1, and
    #   # increment the action_count by 1
    #   Post.update_counters 5, :comment_count => -1, :action_count => 1
    #   # Executes the following SQL:
    #   # UPDATE posts
    #   #    SET comment_count = COALESCE(comment_count, 0) - 1,
    #   #        action_count = COALESCE(action_count, 0) + 1
    #   #  WHERE id = 5
    #
    #   # For the Posts with id of 10 and 15, increment the comment_count by 1
    #   Post.update_counters [10, 15], :comment_count => 1
    #   # Executes the following SQL:
    #   # UPDATE posts
    #   #    SET comment_count = COALESCE(comment_count, 0) + 1
    #   #  WHERE id IN (10, 15)
    def update_counters(id, counters)
      updates = counters.map do |counter_name, value|
        operator = value < 0 ? '-' : '+'
        quoted_column = connection.quote_column_name(counter_name)
        "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
      end

      IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled?

      update_all(updates.join(', '), primary_key => id )
    end

    # Increment a number field by one, usually representing a count.
    #
    # This is used for caching aggregate values, so that they don't need to be computed every time.
    # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
    # shown it would have to run an SQL query to find how many posts and comments there are.
    #
    # ==== Parameters
    #
    # * +counter_name+ - The name of the field that should be incremented.
    # * +id+ - The id of the object that should be incremented.
    #
    # ==== Examples
    #
    #   # Increment the post_count column for the record with an id of 5
    #   DiscussionBoard.increment_counter(:post_count, 5)
    def increment_counter(counter_name, id)
      update_counters(id, counter_name => 1)
    end

    # Decrement a number field by one, usually representing a count.
    #
    # This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
    #
    # ==== Parameters
    #
    # * +counter_name+ - The name of the field that should be decremented.
    # * +id+ - The id of the object that should be decremented.
    #
    # ==== Examples
    #
    #   # Decrement the post_count column for the record with an id of 5
    #   DiscussionBoard.decrement_counter(:post_count, 5)
    def decrement_counter(counter_name, id)
      update_counters(id, counter_name => -1)
    end
  end
end