Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conditional api #390

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have an example with unless as well.

Copy link
Author

@tatethurston tatethurston Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Should I continue this pattern to expand out the variations?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically, symbol, proc, array of symbols, array of procs

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think so

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:
Expand Down
20 changes: 19 additions & 1 deletion lib/counter_culture/counter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty neat!

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]
Expand Down
19 changes: 19 additions & 0 deletions spec/counter_culture_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need a test for the normal updating on insert / update / delete as well, right? I don't even think we change fix_counts in this PR, that's just based on column_names so I don't even think this test here adds any test coverage we don't already have.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree with sidestepping the fix counts logic since that's orthogonal. I tried to think of a way to have fix_count work well out of the box with this approach, but it would require counting in ruby instead of sql.

# 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
Expand Down
8 changes: 8 additions & 0 deletions spec/models/conditional_dependent_shorthand.rb
Original file line number Diff line number Diff line change
@@ -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: -> { {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need a test for unless and for the case of passing a Proc as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, and when passing an array of symbols for if / unless, and when using if and unless together.

ConditionalDependentShorthand.condition => :conditional_dependent_shorthands_count,
} }
end
1 change: 1 addition & 0 deletions spec/models/conditional_main.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
class ConditionalMain < ActiveRecord::Base
has_many :conditional_dependents
has_many :conditional_dependent_shorthands
end
8 changes: 8 additions & 0 deletions spec/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down