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/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