Module: CounterCulture::ActiveRecord::ClassMethods

Defined in:
lib/counter_culture.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#after_commit_counter_cacheObject (readonly)

this holds all configuration data



14
15
16
# File 'lib/counter_culture.rb', line 14

def after_commit_counter_cache
  @after_commit_counter_cache
end

Instance Method Details

#counter_culture(relation, options = {}) ⇒ Object

called to configure counter caches



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/counter_culture.rb', line 17

def counter_culture(relation, options = {})
  unless @after_commit_counter_cache
    # initialize callbacks only once
    after_create :_update_counts_after_create
    after_destroy :_update_counts_after_destroy
    after_update :_update_counts_after_update

    # we keep a list of all counter caches we must maintain
    @after_commit_counter_cache = []
  end

  # add the current information to our list
  @after_commit_counter_cache<< {
    :relation => relation.is_a?(Enumerable) ? relation : [relation],
    :counter_cache_name => (options[:column_name] || "#{name.tableize}_count"),
    :column_names => options[:column_names],
    :delta_column => options[:delta_column],
    :foreign_key_values => options[:foreign_key_values]
  }
end

#counter_culture_fix_counts(options = {}) ⇒ Object

checks all of the declared counter caches on this class for correctnes based on original data; if the counter cache is incorrect, sets it to the correct count

options:

{ :exclude => list of relations to skip when fixing counts,
  :only => only these relations will have their counts fixed }

returns: a list of fixed record as an array of hashes of the form:

{ :entity => which model the count was fixed on,
  :id => the id of the model that had the incorrect count,
  :what => which column contained the incorrect count,
  :wrong => the previously saved, incorrect count,
  :right => the newly fixed, correct count }


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
# File 'lib/counter_culture.rb', line 52

def counter_culture_fix_counts(options = {})
  raise "No counter cache defined on #{self.name}" unless @after_commit_counter_cache

  options[:exclude] = [options[:exclude]] if options[:exclude] && !options[:exclude].is_a?(Enumerable)
  options[:exclude] = options[:exclude].try(:map) {|x| x.is_a?(Enumerable) ? x : [x] }
  options[:only] = [options[:only]] if options[:only] && !options[:only].is_a?(Enumerable)
  options[:only] = options[:only].try(:map) {|x| x.is_a?(Enumerable) ? x : [x] }

  fixed = []
  @after_commit_counter_cache.each do |hash|
    next if options[:exclude] && options[:exclude].include?(hash[:relation])
    next if options[:only] && !options[:only].include?(hash[:relation])

    if options[:skip_unsupported]
      next if (hash[:foreign_key_values] || (hash[:counter_cache_name].is_a?(Proc) && !hash[:column_names]))
    else
      raise "Fixing counter caches is not supported when using :foreign_key_values; you may skip this relation with :skip_unsupported => true" if hash[:foreign_key_values]
      raise "Must provide :column_names option for relation #{hash[:relation].inspect} when :column_name is a Proc; you may skip this relation with :skip_unsupported => true" if hash[:counter_cache_name].is_a?(Proc) && !hash[:column_names]
    end

    # if we're provided a custom set of column names with conditions, use them; just use the
    # column name otherwise
    # which class does this relation ultimately point to? that's where we have to start
    klass = relation_klass(hash[:relation])

    # we are only interested in the id and the count of related objects (that's this class itself)
    query = hash[:delta_column] \
      ? klass.select("#{klass.table_name}.id, SUM(COALESCE(#{self.table_name}.#{hash[:delta_column]},0)) AS count") \
      : klass.select("#{klass.table_name}.id, COUNT(#{self.table_name}.id                              ) AS count")
    query = query.group("#{klass.table_name}.id")
    # respect the deleted_at column if it exists
    query = query.where("#{self.table_name}.deleted_at IS NULL") if self.column_names.include?('deleted_at')

    column_names = hash[:column_names] || {nil => hash[:counter_cache_name]}
    raise ":column_names must be a Hash of conditions and column names" unless column_names.is_a?(Hash)

    # iterate over all the possible counter cache column names
    column_names.each do |where, column_name|
      # if there are additional conditions, add them here
      counts = query.where(where)

      # we need to work our way back from the end-point of the relation to this class itself;
      # make a list of arrays pointing to the second-to-last, third-to-last, etc.
      reverse_relation = []
      (1..hash[:relation].length).to_a.reverse.each {|i| reverse_relation<< hash[:relation][0,i] }

      # we need to join together tables until we get back to the table this class itself
      # lives in
      reverse_relation.each do |cur_relation|
        reflect = relation_reflect(cur_relation)
        counts = counts.joins("JOIN #{reflect.active_record.table_name} ON #{reflect.table_name}.id = #{reflect.active_record.table_name}.#{reflect.foreign_key}")
      end
      # and then we collect the counts in an id => count hash
      counts = counts.inject({}){|memo, model| memo[model.id] = model.count.to_i; memo}

      # now that we know what the correct counts are, we need to iterate over all instances
      # and check whether the count is correct; if not, we correct it
      klass.find_each do |model|
        if model.read_attribute(column_name) != counts[model.id].to_i
          # keep track of what we fixed, e.g. for a notification email
          fixed<< {
            :entity => klass.name,
            :id => model.id,
            :what => column_name,
            :wrong => model.send(column_name),
            :right => counts[model.id]
          }
          # use update_all because it's faster and because a fixed counter-cache shouldn't
          # update the timestamp
          klass.where(:id => model.id).update_all(column_name => counts[model.id].to_i)
        end
      end
    end
  end

  return fixed
end