Module: CounterCulture::ActiveRecord::ClassMethods
- Defined in:
- lib/counter_culture.rb
Instance Attribute Summary collapse
-
#after_commit_counter_cache ⇒ Object
readonly
this holds all configuration data.
Instance Method Summary collapse
-
#counter_culture(relation, options = {}) ⇒ Object
called to configure counter caches.
-
#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.
Instance Attribute Details
#after_commit_counter_cache ⇒ Object (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 37 |
# File 'lib/counter_culture.rb', line 17 def counter_culture(relation, = {}) 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 => ([:column_name] || "#{name.tableize}_count"), :column_names => [:column_names], :delta_column => [:delta_column], :foreign_key_values => [:foreign_key_values], :touch => [:touch] } 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 }
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 135 136 137 138 139 140 141 142 143 |
# File 'lib/counter_culture.rb', line 53 def counter_culture_fix_counts( = {}) raise "No counter cache defined on #{self.name}" unless @after_commit_counter_cache [:exclude] = [[:exclude]] if [:exclude] && ![:exclude].is_a?(Enumerable) [:exclude] = [:exclude].try(:map) {|x| x.is_a?(Enumerable) ? x : [x] } [:only] = [[:only]] if [:only] && ![:only].is_a?(Enumerable) [:only] = [:only].try(:map) {|x| x.is_a?(Enumerable) ? x : [x] } fixed = [] @after_commit_counter_cache.each do |hash| next if [:exclude] && [:exclude].include?(hash[:relation]) next if [:only] && ![:only].include?(hash[:relation]) if [: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]) query = klass # if a delta column is provided use SUM, otherwise use COUNT count_select = hash[:delta_column] ? "SUM(COALESCE(#{self.table_name}.#{hash[:delta_column]},0))" : "COUNT(#{self.table_name}.#{self.primary_key})" # 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) # 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.inject([]) {|a,i| a << hash[:relation][0,i]; a } # store joins in an array so that we can later apply column-specific conditions joins = reverse_relation.map do |cur_relation| reflect = relation_reflect(cur_relation) joins_query = "LEFT JOIN #{reflect.active_record.table_name} ON #{reflect.table_name}.#{reflect.klass.primary_key} = #{reflect.active_record.table_name}.#{reflect.foreign_key}" # adds 'type' condition to JOIN clause if the current model is a child in a Single Table Inheritance joins_query = "#{joins_query} AND #{reflect.active_record.table_name}.type IN ('#{self.name}')" if self.column_names.include?('type') and not(self.descends_from_active_record?) joins_query end # iterate over all the possible counter cache column names column_names.each do |where, column_name| # select id and count (from above) as well as cache column ('column_name') for later comparison counts_query = query.select("#{klass.table_name}.#{klass.primary_key}, #{count_select} AS count, #{klass.table_name}.#{column_name}") # we need to join together tables until we get back to the table this class itself lives in # conditions must also be applied to the join on which we are counting joins.each_with_index do |join,index| join += " AND (#{sanitize_sql_for_conditions(where)})" if index == joins.size - 1 && where counts_query = counts_query.joins(join) end # iterate in batches; otherwise we might run out of memory when there's a lot of # instances and we try to load all their counts at once start = 0 batch_size = [:batch_size] || 1000 while (records = counts_query.reorder(full_primary_key(klass) + " ASC").offset(start).limit(batch_size).group(full_primary_key(klass)).to_a).any? # now iterate over all the models and see whether their counts are right records.each do |model| count = model.read_attribute('count') || 0 if model.read_attribute(column_name) != count # keep track of what we fixed, e.g. for a notification email fixed<< { :entity => klass.name, klass.primary_key.to_sym => model.send(klass.primary_key), :what => column_name, :wrong => model.send(column_name), :right => count } # use update_all because it's faster and because a fixed counter-cache shouldn't # update the timestamp klass.where(klass.primary_key => model.send(klass.primary_key)).update_all(column_name => count) end end start += batch_size end end end return fixed end |