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 |
# 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] } 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( = {}) 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]) # 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 |