Skip to content

Commit

Permalink
Merge pull request #5 from LukinEgor/feature/add-reset-counters-imple…
Browse files Browse the repository at this point in the history
…mentation

add reset_counters implementation
  • Loading branch information
egor-lukin authored Oct 6, 2022
2 parents 0572b68 + 5e207fd commit fa5b8cb
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 3 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ Using `counter_cache: true` on `belongs_to` associations also works as expected.

## Limitations / TODO

- Add `reset_counters` implementation
- Rails 6 support

## Contributing
Expand Down
41 changes: 41 additions & 0 deletions lib/activerecord_slotted_counters/has_slotted_counter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ def update_counters(id, counters)
updated_counters_count
end

def reset_counters(id, *counters, touch: nil)
registered_counters, unregistered_counters = counters.partition { |name| registered_slotted_counter? slotted_counter_name(name) }

if unregistered_counters.present?
super(id, *unregistered_counters, touch: touch)
end

if registered_counters.present?
reset_slotted_counters(id, *registered_counters, touch: touch)
end

true
end

def slotted_counters
if superclass.respond_to?(:slotted_counters)
superclass.slotted_counters + _slotted_counters
Expand Down Expand Up @@ -100,6 +114,25 @@ def _slotted_counters
@_slotted_counters ||= []
end

def reset_slotted_counters(id, *counters, touch: nil)
object = find(id)

counters.each do |counter_association|
has_many_association = _reflect_on_association(counter_association)
raise ArgumentError, "'#{name}' has no association called '#{counter_association}'" unless has_many_association

counter_name = slotted_counter_name counter_association

ActiveRecord::Base.transaction do
counter_value = object.send(counter_association).count(:all)
updates = {counter_name => counter_value}
remove_counters_records([id], counter_name)
insert_counters_records([id], updates)
touch_attributes([id], touch) if touch.present?
end
end
end

def insert_counters_records(ids, counters)
counters_params = prepare_slotted_counters_params(ids, counters)
on_duplicate_clause = "count = slotted_counters.count + excluded.count"
Expand All @@ -113,6 +146,14 @@ def insert_counters_records(ids, counters)
result.rows.count
end

def remove_counters_records(ids, counter_name)
ActiveRecordSlottedCounters::SlottedCounter.where(
counter_name: counter_name,
associated_record_type: name,
associated_record_id: ids
).delete_all
end

def touch_attributes(ids, touch)
scope = where(id: ids)
return scope.touch_all if touch == true
Expand Down
34 changes: 34 additions & 0 deletions spec/slotted_counter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,27 @@
expect(article.likes_count).to eq(likes_count)
expect(article.comments_count).to eq(comments_count)
end

it "should reset native and slotted counters" do
article = WithSlottedCounter::Article.create!

sql = insert_association_sql(WithSlottedCounter::Like, article.id)
ActiveRecord::Base.connection.execute(sql)

sql = insert_association_sql(WithSlottedCounter::Comment, article.id)
ActiveRecord::Base.connection.execute(sql)

article.reload

expect(article.likes_count).to eq(0)
expect(article.comments_count).to eq(0)

WithSlottedCounter::Article.reset_counters(article.id, :likes, :comments)
article.reload

expect(article.likes_count).to eq(1)
expect(article.comments_count).to eq(1)
end
end

describe "using slotted counter in child model" do
Expand Down Expand Up @@ -94,4 +115,17 @@
expect(article.comments_slotted_counters.loaded?).to be_truthy
end
end

def insert_association_sql(association_class, article_id)
association_table = association_class.arel_table
foreign_key = association_class.reflections["article"].foreign_key
insert_manager = Arel::InsertManager.new
insert_manager.insert([
[association_table[foreign_key], article_id],
[association_table[:created_at], Arel.sql("now()")],
[association_table[:updated_at], Arel.sql("now()")]
])

insert_manager.to_sql
end
end
2 changes: 1 addition & 1 deletion spec/support/models/with_slotted_counter/like.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ module WithSlottedCounter
class Like < ActiveRecord::Base
self.table_name = "with_slotted_counter_likes"

belongs_to :article, counter_cache: true
belongs_to :article, counter_cache: true, class_name: "WithSlottedCounter::Article", foreign_key: :with_slotted_counter_article_id
end
end
67 changes: 66 additions & 1 deletion spec/support/shared_examples_for_cache_counters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@

comment_class.create!(article: article)
expect(article.comments_count).to eq(2)

article.comments.destroy_all

article.reload
Expand All @@ -98,4 +97,70 @@
expect(article.views_count).to eq(0)
end
end

describe "reset_counters interface" do
it "must restore the counter without the datetime field updating" do
article = article_class.create!

sql = insert_comment_sql(comment_class, article.id)
ActiveRecord::Base.connection.execute(sql)

expect(article.comments_count).to eq(0)

previous_specific_updated_at = article.specific_updated_at

article_class.reset_counters(article.id, :comments)
article.reload

expect(article.specific_updated_at).to eq(previous_specific_updated_at)
expect(article.comments_count).to eq(1)
end

it "must restore the counter with the datetime field updating" do
article = article_class.create!

sql = insert_comment_sql(comment_class, article.id)
ActiveRecord::Base.connection.execute(sql)

expect(article.comments_count).to eq(0)

previous_specific_updated_at = article.specific_updated_at

article_class.reset_counters(article.id, :comments, touch: :specific_updated_at)
article.reload

expect(article.specific_updated_at).not_to eq(previous_specific_updated_at)
expect(article.comments_count).to eq(1)
end

it "must restore the counter and clear old value" do
article = article_class.create!

article.comments.create!
expect(article.comments_count).to eq(1)

sql = insert_comment_sql(comment_class, article.id)
ActiveRecord::Base.connection.execute(sql)

expect(article.comments_count).to eq(1)

article_class.reset_counters(article.id, :comments)
article.reload

expect(article.comments_count).to eq(2)
end
end

def insert_comment_sql(comment_class, article_id)
comment_table = comment_class.arel_table
foreign_key = comment_class.reflections["article"].foreign_key
insert_manager = Arel::InsertManager.new
insert_manager.insert([
[comment_table[foreign_key], article_id],
[comment_table[:created_at], Arel.sql("now()")],
[comment_table[:updated_at], Arel.sql("now()")]
])

insert_manager.to_sql
end
end

0 comments on commit fa5b8cb

Please sign in to comment.