I have an activerecord class method scope that returns all when the scope should remain unchanged. However I would expect it to use the counter cache when chaining size to the all scope. Here is an example:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
def self.filtered(only_approved:)
if only_approved
where(approved: true)
else
all
end
end
end
# This does not use the counter cache but should since the scope is unchanged
Post.first.comments.filtered(only_approved: false).size
So it looks like Post.comments.size triggers the counter cache while Post.comments.all.size does not. Is there a way around this?
CodePudding user response:
This happens because of how the counter_cache works. It needs 2 things:
- Add the
counter_cache: trueto the belonging model (Comment) - Add a column
comments_countto the having model (Post)
The column added to the Post model gets updated everytime you create or destroy a model so it will count all existing records on the table. This is the reason why it won't work on a scope (a scope might be useful to filter the resulting records, but the actual column comments_count is still counting the whole table).
As a workaround I'd suggest you to take a look at and see if it can be used for your usecase https://github.com/magnusvk/counter_culture.
From their own repo:
class Product < ActiveRecord::Base
belongs_to :category
scope :awesomes, ->{ where "products.product_type = ?", 'awesome' }
scope :suckys, ->{ where "products.product_type = ?", 'sucky' }
counter_culture :category,
column_name: proc {|model| "#{model.product_type}_count" },
column_names: -> { {
Product.awesomes => :awesome_count,
Product.suckys => :sucky_count
} }
end
CodePudding user response:
The only way I found to deal with this is to pass the scope to the class method and return it if no additional scope is to be added. It's not as clean but it works. Here is the updated code:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
def self.filtered(scope:, only_approved:)
if only_approved
scope.where(approved: true)
else
scope
end
end
end
# This works with counter cache if the scope is returned as is
Comment.filtered(scope: Post.first.comments, only_approved: false).size
