active_record_cache 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/License.txt +674 -0
- data/README.rdoc +57 -0
- data/Rakefile +37 -0
- data/lib/active_record_cache/defaults_handler.rb +15 -0
- data/lib/active_record_cache/record_cache.rb +80 -0
- data/lib/active_record_cache/relation_extension.rb +104 -0
- data/lib/active_record_cache.rb +75 -0
- data/spec/active_record_cache_spec.rb +274 -0
- data/spec/defaults_handler_spec.rb +13 -0
- data/spec/record_cache_spec.rb +53 -0
- data/spec/relation_extension_spec.rb +60 -0
- data/spec/spec_helper.rb +118 -0
- metadata +109 -0
data/README.rdoc
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
= Active Record Cache
|
2
|
+
|
3
|
+
This gem provides a caching layer to ActiveRecord models that can be activated in a variety of ways and scopes. The cache strategy that is implemented will only work when querying for records by their primary key, so it is not applicable to all situations, but does provide good support for the most common use case.
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
To enable caching on a model, simply add a call to +use_record_cache+ to the class definition:
|
8
|
+
|
9
|
+
class MyModel < ActiveRecord::Base
|
10
|
+
use_record_cache
|
11
|
+
end
|
12
|
+
|
13
|
+
To use the cache, simple add +from_cache+ to a finder chain:
|
14
|
+
|
15
|
+
MyModel.where(:id => params[:id]).from_cache
|
16
|
+
|
17
|
+
To make matter easier to use the cache, you can also set a default for a model:
|
18
|
+
|
19
|
+
class MyModel < ActiveRecord::Base
|
20
|
+
use_record_cache :default => true
|
21
|
+
end
|
22
|
+
|
23
|
+
You can use the +from_database+ method on you model finders to force a query to hit the database:
|
24
|
+
|
25
|
+
MyModel.where(:id => params[:id]).from_database
|
26
|
+
|
27
|
+
You can also set the default behavior for using the cache on a model within a block:
|
28
|
+
|
29
|
+
ActiveRecordCache.set_defaults(MyModel => true) do
|
30
|
+
# This will use the cache
|
31
|
+
MyModel.where(:id => params[:id])
|
32
|
+
end
|
33
|
+
|
34
|
+
ActiveRecordCache.set_defaults(MyModel => false) do
|
35
|
+
# This will not use the cache
|
36
|
+
MyModel.where(:id => params[:id])
|
37
|
+
end
|
38
|
+
|
39
|
+
This last behavior is also available as a Rack handler:
|
40
|
+
|
41
|
+
Rails.configuration.middleware.use(ActiveRecordCache::DefaultsHandler, MyModel => true)
|
42
|
+
|
43
|
+
By default, Rails.cache will be used for caching records, but you can specify any implementation of ActiveSupport::Cache::Store you wish.
|
44
|
+
|
45
|
+
== Constraints
|
46
|
+
|
47
|
+
* Only queries specifically instructed to use the cache will use the cache. This value can be made the default behavior for models where you always want to use the cache.
|
48
|
+
|
49
|
+
* Only queries by primary key will hit the cache. Queries may be by more that one primary key.
|
50
|
+
|
51
|
+
* Queries can only use the limit and at most one order by statement. Order by statements must be by a single column and can be either in ascending or descending order. Using any other statements in the finder like joins or select will result in the cache being bypassed.
|
52
|
+
|
53
|
+
* Records will be returned as read only so that they cannot be saved back to the database when they come through the cache. This is a safety measure to prevent stale data from being used to update the database.
|
54
|
+
|
55
|
+
== Expiring
|
56
|
+
|
57
|
+
Entries will be expired from the cache whenever they are updated or destroyed. Entries can also be given a time to live with an :expires_in option on either the cache or on the +use_record_cache+ method call. Note that if you don't use a shared cache among all accessors of the record, the records could become out of sync with the database and even among app servers.
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
desc 'Default: run unit tests'
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'rspec'
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
desc 'Run the unit tests'
|
11
|
+
RSpec::Core::RakeTask.new(:test)
|
12
|
+
rescue LoadError
|
13
|
+
task :test do
|
14
|
+
raise "You must have rspec 2.0 installed to run the tests"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'jeweler'
|
20
|
+
Jeweler::Tasks.new do |gem|
|
21
|
+
gem.name = "active_record_cache"
|
22
|
+
gem.summary = %Q{This gem adds a caching layer to ActiveRecord models when finding them by a numeric primary key.}
|
23
|
+
gem.description = %Q{This gem adds a caching layer to ActiveRecord models when finding them by a numeric primary key.}
|
24
|
+
gem.authors = ["Brian Durand"]
|
25
|
+
gem.email = ["mdobrota@tribune.com", "ddpr@tribune.com"]
|
26
|
+
gem.files = FileList["lib/**/*", "spec/**/*", "bin/**/*", "example/**/*" "README.rdoc", "Rakefile", "License.txt"].to_a
|
27
|
+
gem.has_rdoc = true
|
28
|
+
gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
|
29
|
+
gem.extra_rdoc_files = ["README.rdoc"]
|
30
|
+
gem.add_dependency('activerecord', "~>3.0.5")
|
31
|
+
gem.add_dependency('tribune-sort_by_field', "~>1.0.1")
|
32
|
+
gem.add_development_dependency('rspec', '>= 2.0.0')
|
33
|
+
gem.add_development_dependency('sqlite3')
|
34
|
+
end
|
35
|
+
Jeweler::RubygemsDotOrgTasks.new
|
36
|
+
rescue LoadError
|
37
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActiveRecordCache
|
2
|
+
# Rack handler that sets the default for classes if they will use the cache for queries.
|
3
|
+
class DefaultsHandler
|
4
|
+
def initialize(app, options)
|
5
|
+
@app = app
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
ActiveRecordCache.enable_by_default_on(@options) do
|
11
|
+
@app.call(env)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module ActiveRecordCache
|
2
|
+
class RecordCache
|
3
|
+
attr_reader :cache, :expires_in
|
4
|
+
|
5
|
+
def initialize(klass, options = {})
|
6
|
+
@klass = klass
|
7
|
+
@cache = options[:cache] || ActiveRecordCache.cache
|
8
|
+
@expires_in = options[:expires_in] || @cache.options[:expires_in]
|
9
|
+
@preload = options[:preload]
|
10
|
+
@default = (options[:default].nil? ? false : !!options[:default])
|
11
|
+
end
|
12
|
+
|
13
|
+
def read(ids)
|
14
|
+
ids = ids.first if ids.is_a?(Array) && ids.size == 1
|
15
|
+
if ids.is_a?(Array)
|
16
|
+
keys = []
|
17
|
+
id_key_map = {}
|
18
|
+
ids.each do |id|
|
19
|
+
key = cache_key(id)
|
20
|
+
keys << key
|
21
|
+
id_key_map[id] = key
|
22
|
+
end
|
23
|
+
record_map = @cache.read_multi(*keys)
|
24
|
+
missing_ids = ids.reject{|id| record_map[id_key_map[id]]}
|
25
|
+
unless missing_ids.empty?
|
26
|
+
finder.where(@klass.primary_key => missing_ids).each do |record|
|
27
|
+
key = id_key_map[record.id]
|
28
|
+
record_map[key] = record
|
29
|
+
@cache.write(key, record, :expires_in => @expires_in)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
records = []
|
34
|
+
ids.each do |id|
|
35
|
+
record = record_map[id_key_map[id]]
|
36
|
+
records << record if record
|
37
|
+
end
|
38
|
+
records
|
39
|
+
else
|
40
|
+
[self[ids]]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get a value from the cache by primary key.
|
45
|
+
def [](id)
|
46
|
+
@cache.fetch(cache_key(id), :expires_in => @expires_in) do
|
47
|
+
finder.where(@klass.primary_key => id).first
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Remove an entry from the cache.
|
52
|
+
def expire(id)
|
53
|
+
@cache.delete(cache_key(id))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Generate a cache key for a record.
|
57
|
+
def cache_key(id)
|
58
|
+
"#{@klass.model_name.cache_key}/#{id}"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get the default value for whether the cache is enabled or not.
|
62
|
+
def default
|
63
|
+
defaults = Thread.current[:active_record_cache_defaults]
|
64
|
+
if defaults && defaults.include?(@klass.name)
|
65
|
+
defaults[@klass.name]
|
66
|
+
else
|
67
|
+
@default
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Return a Relation for finding records to put in the cache.
|
74
|
+
def finder
|
75
|
+
relation = @klass.from_database.readonly
|
76
|
+
relation = relation.preload(@preload) if @preload
|
77
|
+
relation
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module ActiveRecordCache
|
2
|
+
module RelationExtension
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
SIMPLE_ORDER_BY = /^([a-z][a-z0-9_]*)( ?((asc|desc)(ending)?)?)$/i
|
6
|
+
|
7
|
+
included do
|
8
|
+
alias_method_chain :to_a, :record_cache
|
9
|
+
alias_method_chain :merge, :record_cache
|
10
|
+
attr_accessor :query_from_cache_value
|
11
|
+
end
|
12
|
+
|
13
|
+
# Calling +from_database+ on a Relation will force the query to bypass the cache.
|
14
|
+
def from_database
|
15
|
+
relation = clone
|
16
|
+
relation.query_from_cache_value = false
|
17
|
+
relation
|
18
|
+
end
|
19
|
+
|
20
|
+
# Calling +from_database+ on a Relation will force the query to bypass the cache.
|
21
|
+
def from_cache
|
22
|
+
relation = clone
|
23
|
+
relation.query_from_cache_value = true
|
24
|
+
relation
|
25
|
+
end
|
26
|
+
|
27
|
+
# Override the to_a method to look in the cache.
|
28
|
+
def to_a_with_record_cache #:nodoc:
|
29
|
+
if !loaded? && cacheable_query?
|
30
|
+
ids = get_where_ids
|
31
|
+
unless ids.blank?
|
32
|
+
records = klass.record_cache.read(ids)
|
33
|
+
records = records[0, limit_value.to_i] if limit_value
|
34
|
+
records = records.sort_by_field(order_values.first) unless order_values.empty?
|
35
|
+
if logger && logger.debug?
|
36
|
+
logger.debug(" #{klass.name} LOAD FROM RECORD CACHE #{to_sql}")
|
37
|
+
end
|
38
|
+
@records = records
|
39
|
+
@loaded = true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
to_a_without_record_cache
|
43
|
+
end
|
44
|
+
|
45
|
+
# Override the merge function so that the query_from_cache_value gets merged.
|
46
|
+
def merge_with_record_cache(relation) #:nodoc:
|
47
|
+
merged_relation = merge_without_record_cache(relation)
|
48
|
+
merged_relation.query_from_cache_value = relation.query_from_cache_value
|
49
|
+
merged_relation
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Return true if the query is cacheable. Queries are only cacheable if there is at most one where clause and that
|
55
|
+
# clause searches only by one or more id's. The query must also not do any joins, offsets, or groupings. Any order
|
56
|
+
# clause must be a simple order by one of the columns.
|
57
|
+
def cacheable_query?
|
58
|
+
return false unless klass.respond_to?(:record_cache)
|
59
|
+
from_cache = (query_from_cache_value.nil? ? klass.record_cache.default : query_from_cache_value)
|
60
|
+
return false unless from_cache
|
61
|
+
return false unless where_values.size == (klass.finder_needs_type_condition? ? 2 : 1)
|
62
|
+
return false unless order_values.blank? || (order_values.size == 1 && order_values.first.to_s.match(SIMPLE_ORDER_BY))
|
63
|
+
select_col = select_values.first
|
64
|
+
select_col = select_col.name if select_col && select_col.respond_to?(:name)
|
65
|
+
select_star = select_values.blank? || (select_values.size == 1 && select_col == "*")
|
66
|
+
return false unless select_star
|
67
|
+
return false unless group_values.blank? && includes_values.blank? && eager_load_values.blank? && preload_values.blank? && joins_values.blank? && having_values.blank? && offset_value.blank?
|
68
|
+
return false unless from_value.blank? && lock_value.blank?
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get the primary key values used in the query. This will only return a value if the query
|
73
|
+
# had exactly one where clause by primary key.
|
74
|
+
def get_where_ids
|
75
|
+
where_hash = where_values_hash.with_indifferent_access
|
76
|
+
if where_hash.size == (klass.finder_needs_type_condition? ? 2 : 1)
|
77
|
+
bind_index = 0
|
78
|
+
Array(where_hash[klass.primary_key]).collect do |id|
|
79
|
+
if id == "?"
|
80
|
+
bind_val = bind_values[bind_index]
|
81
|
+
id = bind_val.last if bind_val
|
82
|
+
bind_index += 1
|
83
|
+
end
|
84
|
+
begin
|
85
|
+
Integer(id)
|
86
|
+
rescue ArgumentError
|
87
|
+
return nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
elsif where_hash.empty? && (!defined?(bind_values) || bind_values.empty?)
|
91
|
+
sql = where_values.first
|
92
|
+
if sql.is_a?(String)
|
93
|
+
pk_column = "#{klass.connection.quote_table_name(klass.table_name)}.#{klass.connection.quote_column_name(klass.primary_key)}"
|
94
|
+
pattern = Regexp.new("^\\s*\\(?\\s*#{Regexp.escape(pk_column)}(\\s*(=\\s*(\\d+))|(\\s+IN\\s+\\(([\\d\\s,]+)\\)))\\s*\\)?\\s*$", true)
|
95
|
+
if sql.match(pattern)
|
96
|
+
single_id = $~[3].to_i if $~[3]
|
97
|
+
multiple_ids = $~[5].split(',').collect{|n| n.to_i} if $~[5]
|
98
|
+
single_id || multiple_ids
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'sort_by_field'
|
4
|
+
|
5
|
+
module ActiveRecordCache
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
autoload :DefaultsHandler, File.expand_path("../active_record_cache/defaults_handler.rb", __FILE__)
|
9
|
+
autoload :RecordCache, File.expand_path("../active_record_cache/record_cache.rb", __FILE__)
|
10
|
+
autoload :RelationExtension, File.expand_path("../active_record_cache/relation_extension.rb", __FILE__)
|
11
|
+
|
12
|
+
included do
|
13
|
+
# Expire the cache entry both after updates and destroys to ensure it is consistent within a transaction.
|
14
|
+
after_destroy :expire_cache_entry
|
15
|
+
after_update :expire_cache_entry
|
16
|
+
|
17
|
+
# Expire the cache again after a transaction to ensure it is consistent after database changes are committed.
|
18
|
+
after_commit :expire_cache_entry
|
19
|
+
after_rollback :expire_cache_entry
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# Get the default cache used by models. Defaults to Rails.cache.
|
24
|
+
def cache
|
25
|
+
unless defined?(@cache)
|
26
|
+
@cache = Rails.cache if defined?(Rails.cache)
|
27
|
+
end
|
28
|
+
@cache
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set the default cache used by models.
|
32
|
+
def cache=(value)
|
33
|
+
@cache = value
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set the default for classes if they should use the cache or not for the duration of a block.
|
37
|
+
# The options hash should be a hash of {class_name => (true|false)}.
|
38
|
+
def enable_by_default_on(options)
|
39
|
+
current_defaults = Thread.current[:active_record_cache_defaults]
|
40
|
+
begin
|
41
|
+
defaults = current_defaults ? current_defaults.dup : {}
|
42
|
+
Thread.current[:active_record_cache_defaults] = defaults
|
43
|
+
options.each do |klass, default|
|
44
|
+
defaults[klass.is_a?(Class) ? klass.name : klass.to_s] = !!default
|
45
|
+
end
|
46
|
+
yield
|
47
|
+
ensure
|
48
|
+
Thread.current[:active_record_cache_defaults] = current_defaults
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Expire the record out of the cache.
|
54
|
+
def expire_cache_entry
|
55
|
+
self.class.expire_cache_entry(id)
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods
|
59
|
+
# Call this method to add a cache to a model. The options allowed are:
|
60
|
+
# * :cache - the ActiveSupport::Cache::Store instance to use. By default it will use Rails.cache.
|
61
|
+
# * :expires_in - the number of seconds until cache entries will be automatically refreshed (defaults to 1 hour)
|
62
|
+
def use_record_cache(options = {})
|
63
|
+
class_attribute :record_cache, :instance_reader => false, :instance_writer => false
|
64
|
+
self.record_cache = RecordCache.new(self, options)
|
65
|
+
scope :from_database, scoped.from_database
|
66
|
+
scope :from_cache, scoped.from_cache
|
67
|
+
end
|
68
|
+
|
69
|
+
def expire_cache_entry(id)
|
70
|
+
self.record_cache.expire(id)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
ActiveRecord::Relation.send(:include, ActiveRecordCache::RelationExtension) unless ActiveRecord::Relation.include?(ActiveRecordCache::RelationExtension)
|
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveRecordCache do
|
4
|
+
before :each do
|
5
|
+
Rails.cache.clear
|
6
|
+
MY_CACHE.clear
|
7
|
+
ActiveRecordCache::Tester.destroy_all
|
8
|
+
ActiveRecordCache::Test.destroy_all
|
9
|
+
ActiveRecordCache::SubTest.destroy_all
|
10
|
+
ActiveRecordCache::NoCacheTester.destroy_all
|
11
|
+
ActiveRecordCache::TesterNoCacheDefault.destroy_all
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:test_1){ ActiveRecordCache::Test.create(:name => "test_1") }
|
15
|
+
let(:test_2){ ActiveRecordCache::Test.create(:name => "test_2") }
|
16
|
+
let(:test_3){ ActiveRecordCache::Test.create(:name => "test_3") }
|
17
|
+
let(:subtest_1){ ActiveRecordCache::SubTest.create(:name => "subtest_1") }
|
18
|
+
let(:subtest_2){ ActiveRecordCache::SubTest.create(:name => "subtest_2") }
|
19
|
+
let(:tester_1){ ActiveRecordCache::Tester.create!(:name => "tester_1", :test => test_1) }
|
20
|
+
let(:tester_2){ ActiveRecordCache::Tester.create!(:name => "tester_2", :test => test_1) }
|
21
|
+
let(:tester_3){ ActiveRecordCache::Tester.create!(:name => "tester_3", :test => test_2) }
|
22
|
+
let(:tester_4){ ActiveRecordCache::Tester.create!(:name => "tester_4", :test => test_2) }
|
23
|
+
let(:no_cache_tester_1){ ActiveRecordCache::NoCacheTester.create!(:name => "no_cache_tester_1") }
|
24
|
+
let(:no_cache_tester_2){ ActiveRecordCache::NoCacheTester.create!(:name => "no_cache_tester_2") }
|
25
|
+
let(:no_cache_default_tester_1){ ActiveRecordCache::TesterNoCacheDefault.create!(:name => "no_cache_default_tester_1") }
|
26
|
+
let(:no_cache_default_tester_2){ ActiveRecordCache::TesterNoCacheDefault.create!(:name => "no_cache_default_tester_2") }
|
27
|
+
|
28
|
+
it "should have a cache that defaults to Rails.cache" do
|
29
|
+
ActiveRecordCache.cache.should == Rails.cache
|
30
|
+
begin
|
31
|
+
my_cache = ActiveSupport::Cache::MemoryStore.new
|
32
|
+
ActiveRecordCache.cache = my_cache
|
33
|
+
ActiveRecordCache.cache.should == my_cache
|
34
|
+
ensure
|
35
|
+
ActiveRecordCache.cache = Rails.cache
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "injecting behavior into a model" do
|
40
|
+
it "should be able to add caching to a model" do
|
41
|
+
ActiveRecordCache::Tester.record_cache.cache.should == Rails.cache
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should be able to specify the cache to use" do
|
45
|
+
ActiveRecordCache::Test.record_cache.cache.should == MY_CACHE
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should be able to specify the number of seconds until entries expire" do
|
49
|
+
ActiveRecordCache::Tester.record_cache.expires_in.should == nil
|
50
|
+
ActiveRecordCache::Test.record_cache.expires_in.should == 30
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should have a scope to find from the database" do
|
54
|
+
scope = ActiveRecordCache::Tester.from_database
|
55
|
+
scope.should be_a(ActiveRecord::Relation)
|
56
|
+
scope.query_from_cache_value.should == false
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should have a scope to find from the cache" do
|
60
|
+
scope = ActiveRecordCache::TesterNoCacheDefault.from_cache
|
61
|
+
scope.should be_a(ActiveRecord::Relation)
|
62
|
+
scope.query_from_cache_value.should == true
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should be able to expire a cache entry" do
|
66
|
+
key = ActiveRecordCache::Tester.record_cache.cache_key(1)
|
67
|
+
Rails.cache.write(key, "boo")
|
68
|
+
Rails.cache.read(key).should == "boo"
|
69
|
+
ActiveRecordCache::Tester.expire_cache_entry(1)
|
70
|
+
Rails.cache.exist?(key).should == false
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should expire an entry when a record is destroyed" do
|
74
|
+
key = ActiveRecordCache::Tester.record_cache.cache_key(tester_1.id)
|
75
|
+
Rails.cache.write(key, "boo")
|
76
|
+
Rails.cache.exist?(key).should == true
|
77
|
+
tester_1.destroy
|
78
|
+
Rails.cache.exist?(key).should == false
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should expire an entry when a record is updated" do
|
82
|
+
key = ActiveRecordCache::Tester.record_cache.cache_key(tester_1.id)
|
83
|
+
Rails.cache.write(key, "boo")
|
84
|
+
Rails.cache.exist?(key).should == true
|
85
|
+
tester_1.update_attribute(:name, "new value")
|
86
|
+
Rails.cache.exist?(key).should == false
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should inherit caching" do
|
90
|
+
ActiveRecordCache::SubTest.record_cache.should == ActiveRecordCache::Test.record_cache
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context "default behavior" do
|
95
|
+
it "should not use the cache by default" do
|
96
|
+
ActiveRecordCache::TesterNoCacheDefault.find(no_cache_default_tester_1.id).should == no_cache_default_tester_1
|
97
|
+
Rails.cache.should be_empty
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should use the cache if the default is set to true" do
|
101
|
+
ActiveRecordCache::Tester.find(tester_1.id).should == tester_1
|
102
|
+
Rails.cache.size.should == 1
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should be able to set the defaults in a block" do
|
106
|
+
ActiveRecordCache::Tester.record_cache.default.should == true
|
107
|
+
ActiveRecordCache::Test.record_cache.default.should == true
|
108
|
+
ActiveRecordCache::TesterNoCacheDefault.record_cache.default.should == false
|
109
|
+
|
110
|
+
ActiveRecordCache.enable_by_default_on(ActiveRecordCache::Tester => false, "ActiveRecordCache::TesterNoCacheDefault" => true) do
|
111
|
+
ActiveRecordCache::Tester.record_cache.default.should == false
|
112
|
+
ActiveRecordCache::Test.record_cache.default.should == true
|
113
|
+
ActiveRecordCache::TesterNoCacheDefault.record_cache.default.should == true
|
114
|
+
|
115
|
+
ActiveRecordCache.enable_by_default_on(ActiveRecordCache::Test => false, "ActiveRecordCache::Tester" => true) do
|
116
|
+
ActiveRecordCache::Tester.record_cache.default.should == true
|
117
|
+
ActiveRecordCache::Test.record_cache.default.should == false
|
118
|
+
ActiveRecordCache::TesterNoCacheDefault.record_cache.default.should == true
|
119
|
+
end
|
120
|
+
|
121
|
+
ActiveRecordCache::Tester.record_cache.default.should == false
|
122
|
+
ActiveRecordCache::Test.record_cache.default.should == true
|
123
|
+
ActiveRecordCache::TesterNoCacheDefault.record_cache.default.should == true
|
124
|
+
end
|
125
|
+
|
126
|
+
ActiveRecordCache::Tester.record_cache.default.should == true
|
127
|
+
ActiveRecordCache::Test.record_cache.default.should == true
|
128
|
+
ActiveRecordCache::TesterNoCacheDefault.record_cache.default.should == false
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context "caching" do
|
133
|
+
before :each do
|
134
|
+
test_1
|
135
|
+
test_2
|
136
|
+
test_3
|
137
|
+
subtest_1
|
138
|
+
subtest_2
|
139
|
+
tester_1
|
140
|
+
tester_2
|
141
|
+
tester_3
|
142
|
+
tester_4
|
143
|
+
no_cache_tester_1
|
144
|
+
no_cache_tester_2
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should lazy load a single record into the cache" do
|
148
|
+
ActiveRecordCache::Tester.find(tester_1.id).should == tester_1
|
149
|
+
ActiveRecordCache::Tester.connection.should_not_receive(:select)
|
150
|
+
ActiveRecordCache::Tester.find(tester_1.id).should == tester_1
|
151
|
+
ActiveRecordCache::Tester.find(tester_1.id.to_s).should == tester_1
|
152
|
+
ActiveRecordCache::Tester.find_by_id(tester_1.id).should == tester_1
|
153
|
+
ActiveRecordCache::Tester.where(:id => tester_1.id).should == [tester_1]
|
154
|
+
ActiveRecordCache::Tester.where("id" => tester_1.id.to_s).should == [tester_1]
|
155
|
+
c = ActiveRecordCache::Tester.connection
|
156
|
+
sql = "#{c.quote_table_name(ActiveRecordCache::Tester.table_name)}.#{c.quote_column_name('id')} = #{tester_1.id}"
|
157
|
+
ActiveRecordCache::Tester.where(sql).should == [tester_1]
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should lazy load multiple records into the cache" do
|
161
|
+
ActiveRecordCache::Tester.find(tester_1.id, tester_2.id).sort_by_field(:name).should == [tester_1, tester_2]
|
162
|
+
ActiveRecordCache::Tester.connection.should_not_receive(:select)
|
163
|
+
ActiveRecordCache::Tester.find(tester_1.id, tester_2.id).sort_by_field(:name).should == [tester_1, tester_2]
|
164
|
+
ActiveRecordCache::Tester.find_all_by_id([tester_1.id, tester_2.id]).sort_by_field(:name).should == [tester_1, tester_2]
|
165
|
+
ActiveRecordCache::Tester.where(:id => [tester_1.id, tester_2.id]).should == [tester_1, tester_2]
|
166
|
+
ActiveRecordCache::Tester.where("id" => [tester_1.id.to_s, tester_2.id.to_s]).should == [tester_1, tester_2]
|
167
|
+
c = ActiveRecordCache::Tester.connection
|
168
|
+
sql = "#{c.quote_table_name(ActiveRecordCache::Tester.table_name)}.#{c.quote_column_name('id')} IN (#{tester_1.id}, #{tester_2.id})"
|
169
|
+
ActiveRecordCache::Tester.where(sql).should == [tester_1, tester_2]
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should only load records not already in the cache from the database" do
|
173
|
+
ActiveRecordCache::Tester.find(tester_1.id).should == tester_1
|
174
|
+
key = ActiveRecordCache::Tester.record_cache.cache_key(tester_1.id)
|
175
|
+
first_cache_entry = Rails.cache[key]
|
176
|
+
ActiveRecordCache::Tester.find(tester_1.id, tester_2.id).sort_by_field(:name).should == [tester_1, tester_2]
|
177
|
+
Rails.cache[key].object_id.should == first_cache_entry.object_id
|
178
|
+
Rails.cache.size.should == 2
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should cache when limit is set" do
|
182
|
+
found = ActiveRecordCache::Tester.where(:id => [tester_1.id, tester_2.id]).limit(1).first
|
183
|
+
[tester_1, tester_2].should include(found)
|
184
|
+
Rails.cache.size.should == 2
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should cache when ordering is by a single column" do
|
188
|
+
found = ActiveRecordCache::Tester.where(:id => [tester_1.id, tester_2.id]).order(:name).should == [tester_1, tester_2]
|
189
|
+
Rails.cache.size.should == 2
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should cache when ordering is by a single column with a direction" do
|
193
|
+
found = ActiveRecordCache::Tester.where(:id => [tester_1.id, tester_2.id]).order("name DESC").should == [tester_2, tester_1]
|
194
|
+
Rails.cache.size.should == 2
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should cache when loading belongs_to associations" do
|
198
|
+
records = ActiveRecordCache::Tester.where(:id => [tester_1.id, tester_2.id, tester_3.id]).order(:name).preload(:test)
|
199
|
+
records.should == [tester_1, tester_2, tester_3]
|
200
|
+
ActiveRecordCache::Test.connection.should_not_receive(:select)
|
201
|
+
records.all?{|r| r.test_association_loaded? }.should == true
|
202
|
+
ActiveRecordCache::Test.find(tester_1.test_id).should == test_1
|
203
|
+
ActiveRecordCache::Test.find(tester_2.test_id).should == test_1
|
204
|
+
ActiveRecordCache::Test.find(tester_3.test_id).should == test_2
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should preload specified associations" do
|
208
|
+
tester_1.no_cache_testers.create(:name => "relation_1")
|
209
|
+
tester_1.no_cache_testers.create(:name => "relation_2")
|
210
|
+
record = ActiveRecordCache::Tester.find(tester_1.id)
|
211
|
+
record.no_cache_testers.loaded?.should == true
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should cache when using polymorphic tables" do
|
215
|
+
ActiveRecordCache::SubTest.find(subtest_1.id).should == subtest_1
|
216
|
+
ActiveRecordCache::SubTest.connection.should_not_receive(:select)
|
217
|
+
ActiveRecordCache::SubTest.where(:id => subtest_1.id).should == [subtest_1]
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should not cache when force loading from the database" do
|
221
|
+
ActiveRecordCache::Tester.from_database.where(:id => tester_1.id).should == [tester_1]
|
222
|
+
Rails.cache.should be_empty
|
223
|
+
end
|
224
|
+
|
225
|
+
it "should not cache when there are multiple elements to a where clause hash" do
|
226
|
+
ActiveRecordCache::Tester.from_database.where(:id => tester_1.id, :name => "tester_1").should == [tester_1]
|
227
|
+
ActiveRecordCache::Tester.from_database.where(:id => tester_1.id).where(:name => "tester_1").should == [tester_1]
|
228
|
+
Rails.cache.should be_empty
|
229
|
+
end
|
230
|
+
|
231
|
+
it "should not cache when there are mulitple clauses in a sql string where clause" do
|
232
|
+
c = ActiveRecordCache::Tester.connection
|
233
|
+
sql = "#{c.quote_table_name(ActiveRecordCache::Tester.table_name)}.#{c.quote_column_name('id')} = #{tester_1.id} AND name = 'tester_1'"
|
234
|
+
ActiveRecordCache::Tester.from_database.where(sql).should == [tester_1]
|
235
|
+
Rails.cache.should be_empty
|
236
|
+
end
|
237
|
+
|
238
|
+
it "should not cache when ordering is more complex than a single column with a direction" do
|
239
|
+
ActiveRecordCache::Tester.where(:id => [tester_1.id, tester_2.id]).order(:name).order(:id).should == [tester_1, tester_2]
|
240
|
+
ActiveRecordCache::Tester.where(:id => [tester_1.id, tester_2.id]).order("name, id").should == [tester_1, tester_2]
|
241
|
+
Rails.cache.should be_empty
|
242
|
+
end
|
243
|
+
|
244
|
+
it "should not cache with offset is set" do
|
245
|
+
ActiveRecordCache::Tester.where(:id => [tester_1.id, tester_2.id]).order(:name).offset(1).should == [tester_2]
|
246
|
+
Rails.cache.should be_empty
|
247
|
+
end
|
248
|
+
|
249
|
+
it "should not cache when joins are used" do
|
250
|
+
ActiveRecordCache::Tester.where(:id => tester_1.id).joins(:test).should == [tester_1]
|
251
|
+
Rails.cache.should be_empty
|
252
|
+
end
|
253
|
+
|
254
|
+
it "should not cache when select is used" do
|
255
|
+
ActiveRecordCache::Tester.where(:id => tester_1.id).select(:id).should == [tester_1]
|
256
|
+
Rails.cache.should be_empty
|
257
|
+
end
|
258
|
+
|
259
|
+
it "should not cache when from is used" do
|
260
|
+
ActiveRecordCache::Tester.where(:id => tester_1.id).from(ActiveRecordCache::Tester.table_name).should == [tester_1]
|
261
|
+
Rails.cache.should be_empty
|
262
|
+
end
|
263
|
+
|
264
|
+
it "should not cache when grouping is used" do
|
265
|
+
ActiveRecordCache::Tester.where(:id => tester_1.id).group(:name).should == [tester_1]
|
266
|
+
Rails.cache.should be_empty
|
267
|
+
end
|
268
|
+
|
269
|
+
it "should not cache when having is used" do
|
270
|
+
ActiveRecordCache::Tester.where(:id => tester_1.id).group(:name).having(:name => "tester_1").should == [tester_1]
|
271
|
+
Rails.cache.should be_empty
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveRecordCache::DefaultsHandler do
|
4
|
+
it "should set the defaults for classes withing a rack request" do
|
5
|
+
ActiveRecordCache::Tester.record_cache.default.should == true
|
6
|
+
ActiveRecordCache::Test.record_cache.default.should == true
|
7
|
+
ActiveRecordCache::TesterNoCacheDefault.record_cache.default.should == false
|
8
|
+
app = lambda{|env| [200, {:tester => ActiveRecordCache::Tester.record_cache.default, :test => ActiveRecordCache::Test.record_cache.default, :no_cache => ActiveRecordCache::TesterNoCacheDefault.record_cache.default}, env.inspect]}
|
9
|
+
handler = ActiveRecordCache::DefaultsHandler.new(app, ActiveRecordCache::Tester => false, "ActiveRecordCache::TesterNoCacheDefault" => true)
|
10
|
+
env = {"hello" => "world"}
|
11
|
+
handler.call(env).should == [200, {:tester => false, :test => true, :no_cache => true}, env.inspect]
|
12
|
+
end
|
13
|
+
end
|