diff --git a/README.md b/README.md index a3712af..25a81a3 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,24 @@ Now, the ```Category``` model will keep the counter cache in ```special_count``` If you would like to use this with `counter_culture_fix_counts`, make sure to also provide [the `column_names` configuration](#handling-dynamic-column-names). +### Conditional counter cache shorthand + +You may also use a conditional API identical to [ActiveRecord's Callbacks](https://guides.rubyonrails.org/active_record_callbacks.html#conditional-callbacks) to conditionally count columns: + +```ruby +class Product < ActiveRecord::Base + belongs_to :category + counter_culture :category, column_name: :special_count, if: :special? +end + +class Category < ActiveRecord::Base + has_many :products +end + +Now, the ```Category``` model will keep the counter cache in ```special_count``` up-to-date. Only products where ```special?``` returns true will affect the special_count. + +If you would like to use this with `counter_culture_fix_counts`, make sure to also provide [the `column_names` configuration](#handling-dynamic-column-names). + ### Temporarily skipping counter cache updates If you would like to temporarily pause counter_culture, for example in a backfill script, you can do so as follows: diff --git a/lib/counter_culture/counter.rb b/lib/counter_culture/counter.rb index a829391..ecb10a6 100644 --- a/lib/counter_culture/counter.rb +++ b/lib/counter_culture/counter.rb @@ -9,7 +9,25 @@ def initialize(model, relation, options) @model = model @relation = relation.is_a?(Enumerable) ? relation : [relation] - @counter_cache_name = options.fetch(:column_name, "#{model.name.demodulize.tableize}_count") + @counter_cache_name = proc do |model| + conditions = proc do |condition| + case condition + when Symbol + model.public_send(condition) + when Proc + model.instance_exec(&condition) + else + raise ArgumentError, "Condition must be a symbol or a proc" + end + end + + conditions_allow_change? = Array.wrap(options[:if]).all?(&conditions) && + Array.wrap(options[:unless]).none?(&conditions) + return unless conditions_allow_change? + + options.fetch(:column_name, "#{model.name.demodulize.tableize}_count") + end + @column_names = options[:column_names] @delta_column = options[:delta_column] @foreign_key_values = options[:foreign_key_values] diff --git a/spec/counter_culture_spec.rb b/spec/counter_culture_spec.rb index 9b56554..24b3dab 100644 --- a/spec/counter_culture_spec.rb +++ b/spec/counter_culture_spec.rb @@ -15,6 +15,7 @@ require 'models/simple_dependent' require 'models/conditional_main' require 'models/conditional_dependent' +require 'models/conditional_dependent_shorthand' require 'models/post' require 'models/post_comment' require 'models/post_like' @@ -1711,6 +1712,24 @@ def yaml_load(yaml) ConditionalMain.find_each { |main| expect(main.conditional_dependents_count).to eq(main.id % 2 == 0 ? 3 : 0) } end + it "should correctly fix the counter caches for thousands of records when counter is conditional using :if/:unless" do + # first, clean up + ConditionalDependentShorthand.delete_all + ConditionalMain.delete_all + + MANY.times do |i| + main = ConditionalMain.create + 3.times { main.conditional_dependent_shorthands.create(:condition => main.id % 2 == 0) } + end + + ConditionalMain.find_each { |main| expect(main.conditional_dependent_shorthands_count).to eq(main.id % 2 == 0 ? 3 : 0) } + + ConditionalMain.order(db_random).limit(A_FEW).update_all :conditional_dependent_shorthands_count => 1 + ConditionalDependentShorthand.counter_culture_fix_counts :batch_size => A_BATCH + + ConditionalMain.find_each { |main| expect(main.conditional_dependent_shorthands_count).to eq(main.id % 2 == 0 ? 3 : 0) } + end + it "should correctly fix the counter caches when no dependent record exists for some of main records" do # first, clean up SimpleDependent.delete_all diff --git a/spec/models/conditional_dependent_shorthand.rb b/spec/models/conditional_dependent_shorthand.rb new file mode 100644 index 0000000..8aa1449 --- /dev/null +++ b/spec/models/conditional_dependent_shorthand.rb @@ -0,0 +1,8 @@ +class ConditionalDependentShorthand < ActiveRecord::Base + belongs_to :conditional_main + scope :condition, -> { where(condition: true) } + + counter_culture :conditional_main, if: :condition?, column_names: -> { { + ConditionalDependentShorthand.condition => :conditional_dependent_shorthands_count, + } } +end diff --git a/spec/models/conditional_main.rb b/spec/models/conditional_main.rb index 06dacdb..8c24c55 100644 --- a/spec/models/conditional_main.rb +++ b/spec/models/conditional_main.rb @@ -1,3 +1,4 @@ class ConditionalMain < ActiveRecord::Base has_many :conditional_dependents + has_many :conditional_dependent_shorthands end diff --git a/spec/schema.rb b/spec/schema.rb index b2c46c9..0f1b5ed 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -139,6 +139,7 @@ create_table "conditional_mains", :force => true do |t| t.integer "conditional_dependents_count", :null => false, :default => 0 + t.integer "conditional_dependent_shorthands_count", :null => false, :default => 0 t.datetime "created_at" t.datetime "updated_at" end @@ -150,6 +151,13 @@ t.datetime "updated_at" end + create_table "conditional_dependent_shorthands", :force => true do |t| + t.integer "conditional_main_id" + t.boolean "condition", default: false + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "categs", :primary_key => "cat_id", :force => true do |t| t.integer "posts_count", :default => 0, :null => false t.datetime "created_at"