Skip to content

Instantly share code, notes, and snippets.

@estum
Last active April 22, 2016 22:00
Show Gist options
  • Save estum/6db8cbd09e846adf52da2dde5b6293d5 to your computer and use it in GitHub Desktop.
Save estum/6db8cbd09e846adf52da2dde5b6293d5 to your computer and use it in GitHub Desktop.
CachedCount: the ActiveRecord::Relation extension to speed up query records
require "redis"
require "cityhash"
module CachedCount
mattr_accessor(:ttl, instance_accessors: false) { 60 }
mattr_accessor(:redis, instance_accessors: false) { Redis.new }
class << self
def flush!
keys = redis.scan_each(match: "cached_count/*").to_a
keys.present? && Kefir.redis.del(*keys)
end
def fetch(key)
if from_cache = redis.get(key)
from_cache.to_i
else
yield.tap { |value| redis.set(key, value, nx: true, ex: ttl) }
end
end
end
def count(column = nil, options = nil)
column = primary_key if column.nil? || column == :all
if limited_scope?
records_count
else
sanitized_for_count.cached_count(column)
end
end
def cached_count(column, &block)
@cached_count ||= CachedCount.fetch(cache_key(column)) { calculate(:count, column, nil) }
end
def records_count
@records_count ||= 0
end
def limited_scope?
limit_value.present?
end
protected
def sanitized_for_count?
!@sanitized_for_count.nil?
end
def sanitized_for_count
@sanitized_for_count ||= spawn.sanitized_for_count!
end
def sanitized_for_count!
joins_was = joins_values.size
self.joins_values = joins_values.grep_v(/^LEFT( OUTER)? JOIN/i)
unscope!(:group) if (joins_was - joins_values.size) > 0
unscope!(:select)
self
end
def cache_key(*args)
@cache_key ||= begin
hashed_sql = CityHash.hash64(to_sql.squish!)
conditions = [hashed_sql, *args]
conditions.delete(primary_key)
[table_key, conditions.join(":")].join('/')
end
end
private
def exec_queries
super.tap { @records_count = size }
end
def table_key
@table_key ||= "cached_count/#{table_name}".freeze
end
end
@estum
Copy link
Author

estum commented Apr 22, 2016

CachedCount

ActiveRecord::Relation extension to speed up query records counts with Redis.
It's helpful if you use it with shameless paginator gems, like Kaminari,
especially with ActiveAdmin.

Install:

It requires redis and cityhash gems, ensure you have them in the Gemfile.

Just put the cached_count.rb to your app's directory in the $REQUIRE_PATH,
for example to the lib/cached_count.rb, and require it somewhere, for example,
in the config/initializers/cached_count.rb initializer.

Also, if you using ActiveAdmin and Rails 4, please, add the hook after AA initialized:

# config/initializers/active_admin.rb

ActiveAdmin::Helpers::Collection.module_eval do
  def collection_size(c = nil)
    n = (c || collection).count
    n.is_a?(Hash) ? n.count : n
  end
end

Usage:

# Firstly, set redis:
CachedCount.redis = Redis.new

# Change cache expire time (default: 1 minute)
CachedCount.ttl = 15.seconds

# Extend scoping collection with this module:
posts = Post.where(is_published: true).limit(30).offset(120)
posts = posts.extending(CachedCount)

posts.count # => loads relation and counts loaded records,
            #    without querying "COUNT(*)

posts = posts.unscope(:limit, :offset) # kaminary-style
posts.count # => executes "COUNT(id)" and caches it for 
            #    the query with an expiration time
posts.count # => next time, it loads count for cache

# Drop overall count caches
CachedCount.flush!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment