Permalink
Browse files

Added the ability to cache statistics

  • Loading branch information...
1 parent 31d6d62 commit a0c107d1e505054ac98b190acb0cdbf083418f1b alex committed Dec 16, 2009
Showing with 52 additions and 3 deletions.
  1. +13 −0 README.markdown
  2. +15 −2 lib/statistics.rb
  3. +24 −1 test/statistics_test.rb
View
@@ -47,6 +47,19 @@ Note: I found filtering to be an important part of reporting (ie. filtering by d
# NOTE: filters are optional (ie. no filters will be applied if none are passed in)
Account.get_stat(:user_count)
+#### Caching
+
+This is a new feature that uses `Rails.cache`. You can cache certain statistics for a specified amount of time (see below). By default caching is disabled if you do not pass in the `:cache_for` option. It is also important to note that caching is scoped by filters, there is no way around this since different filters produce different values.
+ class Account < ActiveRecord::Base
+ define_statistic :user_count, :count => :all, :cache_for => 30.minutes, :filter_on { :state => 'state = ?' }
+ end
+
+ Account.statistics(:state => 'NY') # This call generates a SQL query
+
+ Account.statistics(:state => 'NY') # This call and subsequent calls for the next 30 minutes will use the cached value
+
+ Account.statistics(:state => 'PA') # This call generates a SQL query because the user count for NY and PA could be different (and probably is)
+
#### Standardized
All ActiveRecord classes now respond to `statistics` and `get_stat` methods
View
@@ -24,6 +24,7 @@ module HasStats
#* +column_name+ - The SQL column to perform the operation on (default: +id+)
#* +filter_on+ - A hash with keys that represent filters. The with values in the has are rules
# on how to generate the query for the correspond filter.
+ #* +cached_for+ - A duration for how long to cache this specific statistic
#
# Additional options can also be passed in that would normally be passed to an ActiveRecord
# +calculate+ call, like +conditions+, +joins+, etc
@@ -39,6 +40,7 @@ module HasStats
# define_statistic "Chained Scope Count", :count => [:all, :my_scope]
# define_statistic "Default Filter", :count => :all
# define_statistic "Custom Filter", :count => :all, :filter_on => { :channel => 'channel = ?', :start_date => 'DATE(created_at) > ?' }
+ # define_statistic "Cached", :count => :all, :filter_on => { :channel => 'channel = ?', :blah => 'blah = ?' }, :cache_for => 1.second
# end
def define_statistic(name, options)
method_name = name.to_s.gsub(" ", "").underscore + "_stat"
@@ -51,11 +53,16 @@ def define_statistic(name, options)
calculation = options.keys.find {|opt| Statistics::supported_calculations.include?(opt)}
calculation ||= :count
-
+
# We must use the metaclass here to metaprogrammatically define a class method
(class<<self; self; end).instance_eval do
define_method(method_name) do |filters|
+ # check the cache before running a query for the stat
+ cached_val = Rails.cache.read("#{self.name}#{method_name}#{filters}") if options[:cache_for]
+ return cached_val unless cached_val.nil?
+
scoped_options = options.dclone
+
filters.each do |key, value|
if value
sql = ((@filter_all_on || {}).merge(scoped_options[:filter_on] || {}))[key].gsub("?", "'#{value}'")
@@ -76,7 +83,12 @@ def define_statistic(name, options)
scopes.each do |scope|
base = base.send(scope)
end if scopes != [:all]
- base.calculate(calculation, scoped_options[:column_name], sql_options(scoped_options))
+ stat_value = base.calculate(calculation, scoped_options[:column_name], sql_options(scoped_options))
+
+ # cache stat value
+ Rails.cache.write("#{self.name}#{method_name}#{filters}", stat_value, :expires_in => options[:cache_for]) if options[:cache_for]
+
+ stat_value
end
end
end
@@ -160,6 +172,7 @@ def sql_options(options)
end
options.delete(:column_name)
options.delete(:filter_on)
+ options.delete(:cache_for)
options
end
end
@@ -4,11 +4,18 @@
gem 'activerecord', '>= 1.15.4.7794'
gem 'mocha', '>= 0.9.0'
require 'active_record'
+require 'active_support'
require 'mocha'
require "#{File.dirname(__FILE__)}/../init"
-ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
+ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
+
+class Rails
+ def self.cache
+ ActiveSupport::Cache::MemCacheStore.new
+ end
+end
class StatisticsTest < Test::Unit::TestCase
@@ -23,6 +30,7 @@ class MockModel < ActiveRecord::Base
define_statistic "Chained Scope Count", :count => [:all, :named_scope]
define_statistic "Default Filter", :count => :all
define_statistic "Custom Filter", :count => :all, :filter_on => { :channel => 'channel = ?', :start_date => 'DATE(created_at) > ?', :blah => 'blah = ?' }
+ define_statistic "Cached", :count => :all, :filter_on => { :channel => 'channel = ?', :blah => 'blah = ?' }, :cache_for => 1.second
define_calculated_statistic "Total Amount" do
defined_stats('Basic Sum') * defined_stats('Basic Count')
@@ -43,6 +51,7 @@ def test_statistics
MockModel.expects(:chained_scope_count_stat).returns(4)
MockModel.expects(:default_filter_stat).returns(5)
MockModel.expects(:custom_filter_stat).returns(3)
+ MockModel.expects(:cached_stat).returns(9)
MockModel.expects(:total_amount_stat).returns(54)
["Basic Count",
@@ -51,6 +60,7 @@ def test_statistics
"Chained Scope Count",
"Default Filter",
"Custom Filter",
+ "Cached",
"Total Amount"].each do |key|
assert MockModel.statistics_keys.include?(key)
end
@@ -61,6 +71,7 @@ def test_statistics
"Chained Scope Count" => 4,
"Default Filter" => 5,
"Custom Filter" => 3,
+ "Cached" => 9,
"Total Amount" => 54 }, MockModel.statistics)
end
@@ -124,5 +135,17 @@ def test_custom_filter_stat
end.returns(3)
assert_equal 3, MockModel.custom_filter_stat(:channel => 'chan5', :start_date => Date.today.to_s(:db))
end
+
+ def test_cached_stat
+ MockModel.expects(:calculate).returns(6)
+ assert_equal 6, MockModel.cached_stat({:channel => 'chan5'})
+
+ MockModel.stubs(:calculate).returns(8)
+ assert_equal 6, MockModel.cached_stat({:channel => 'chan5'})
+ assert_equal 8, MockModel.cached_stat({})
+
+ sleep(1)
+ assert_equal 8, MockModel.cached_stat({:channel => 'chan5'})
+ end
end

0 comments on commit a0c107d

Please sign in to comment.