Class: CounterCulture::Reconciler

Inherits:
Object
  • Object
show all
Defined in:
lib/counter_culture/reconciler.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(counter, options = {}) ⇒ Reconciler

Returns a new instance of Reconciler.



11
12
13
14
15
16
# File 'lib/counter_culture/reconciler.rb', line 11

def initialize(counter, options={})
  @counter, @options = counter, options

  @changes = []
  @reconciled = false
end

Instance Attribute Details

#changesObject (readonly)

Returns the value of attribute changes.



6
7
8
# File 'lib/counter_culture/reconciler.rb', line 6

def changes
  @changes
end

#counterObject (readonly)

Returns the value of attribute counter.



6
7
8
# File 'lib/counter_culture/reconciler.rb', line 6

def counter
  @counter
end

#optionsObject (readonly)

Returns the value of attribute options.



6
7
8
# File 'lib/counter_culture/reconciler.rb', line 6

def options
  @options
end

Instance Method Details

#reconcile!Object



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

def reconcile!
  return false if @reconciled

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

  scope = relation_class

  # respect the deleted_at column if it exists
  scope = scope.where("#{model.table_name}.deleted_at IS NULL") if model.column_names.include?('deleted_at')

  counter_column_names = column_names || {nil => counter_cache_name}

  # iterate over all the possible counter cache column names
  counter_column_names.each do |where, column_name|
    # if the column name is nil, that means those records don't affect
    # counts; we don't need to do anything in that case. but we allow
    # specifying that condition regardless to make the syntax less
    # confusing
    next unless column_name

    # select join column and count (from above) as well as cache column ('column_name') for later comparison
    counts_query = scope.select("#{relation_class.table_name}.#{relation_class.primary_key}, #{relation_class.table_name}.#{relation_reflect(relation).association_primary_key}, #{count_select} AS count, #{relation_class.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
    join_clauses.each_with_index do |join,index|
      if index == join_clauses.size - 1 && where
        join += " AND (#{model.send(:sanitize_sql_for_conditions, where)})"
      end
      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
    batch_size = options.fetch(:batch_size, CounterCulture.config.batch_size)

    counts_query.group(full_primary_key(relation_class)).find_in_batches(batch_size: batch_size) do |records|
      # now iterate over all the models and see whether their counts are right
      ActiveRecord::Base.transaction do
        records.each do |record|
          count = record.read_attribute('count') || 0
          next if record.read_attribute(column_name) == count

          track_change(record, column_name, count)

          # use update_all because it's faster and because a fixed counter-cache shouldn't update the timestamp
          relation_class.where(relation_class.primary_key => record.send(relation_class.primary_key)).update_all(column_name => count)
        end
      end
    end
  end

  @reconciled = true
end