last_mod_cache 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT_LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Brian Durand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,108 @@
1
+ = LastModCache
2
+
3
+ This module adds a simple caching layer on ActiveRecord models. Models must include a column that contains a timestamp of when the record was last modified (i.e. an +updated_at+ column). This timestamp will be used to automatically invalidate cache entries as records are modified so your cache is always up to date.
4
+
5
+ == Example
6
+
7
+ Any model that includes the LastModCache module will have several "with_cache" methods added for finding records with a caching layer.
8
+
9
+ class MyModelMigration < ActiveRecord::Migration
10
+ def self.up
11
+ create_table :my_models do |t|
12
+ t.string :name
13
+ t.integer :value
14
+ t.timestamps # Include the magical updated_at column
15
+ end
16
+
17
+ # Very important: the updated_at column must be indexed in order to use the caching methods effectively.
18
+ # You will take a big performance hit if this isn't done.
19
+ add_index :my_models, :updated_at
20
+ end
21
+ end
22
+
23
+ class MyModel < ActiveRecord::Base
24
+ include LastModCache
25
+ end
26
+
27
+ # Rails 2 style finders with caching
28
+ MyModel.first_with_cache(:conditions => {:name => "test"})
29
+ MyModel.all_with_cache(:conditions => {:value => 0}, :limit => 10)
30
+
31
+ # Rails 3 style finders with caching
32
+ MyModel.where(:name => "test").limit(10).with_cache
33
+
34
+ # Find by ids with cache
35
+ MyModel.find_with_cache(100)
36
+ MyModel.find_with_cache([100, 101, 102])
37
+
38
+ # Dynamic finders are also available in caching versions
39
+ MyModel.find_by_name_with_cache("test")
40
+ MyModel.find_all_by_name_and_value_with_cache("test", 4)
41
+
42
+ == Configuring
43
+
44
+ By default, the +updated_at+ column will be used for checking time stamps on your records. This can be overridden either by setting the +updated_at_column+ on your model.
45
+
46
+ class MyModel < ActiveRecord::Base
47
+ include LastModCache
48
+ self.updated_at_column = :last_modified_time
49
+ end
50
+
51
+ It is very important that you have an index on the column being used since it will queried on directly as part of the caching algorithm.
52
+
53
+ The default Rails.cache will be used if available. If you'd like to specify a different cache for your models, you can specify it by setting +last_mod_cache+ on your model.
54
+
55
+ class MyModel < ActiveRecord::Base
56
+ include LastModCache
57
+ self.last_mod_cache = ActiveSupport::Cache::MemoryStore.new
58
+ end
59
+
60
+ The cache can be any implementation of ActiveSupport::Cache::CacheStore.
61
+
62
+ == Performance Notes
63
+
64
+ This module provides a very easy method of adding caching to your models, but not necessarily the most efficient. It is intended to provide a quick method of boosting performance and reducing database load. In critical sections of your code, you may want to handle caching differently.
65
+
66
+ The response from all of the +with_cache+ methods will be a lazy loaded only be when necessary (similar to how the ActiveRecord 3 finder methods work). This allows them to interact nicely with other caching layers so that you're not loading records that are never used.
67
+
68
+ The objects returned from the cache will be frozen. If you'll need to modify a record, don't use the cache.
69
+
70
+ === Eager Loading Associations
71
+
72
+ Any eager loaded associations on the model will be loaded before the value is cached. This can really boost performance for complex data structures. However, associations are *not* included in the timestamp calculation, so use with caution since you could get stale associations from the cache. If your code is susceptible to this condition, you can work around it be providing a call back in you associated model that calls <tt>update_timestamp!</tt>.
73
+
74
+ class MyModel < ActiveRecord::Base
75
+ include LastModCache
76
+ has_many :my_associations
77
+ end
78
+
79
+ class MyAssociation < ActiveRecord::Base
80
+ belongs_to :my_model
81
+ after_save do |record|
82
+ record.my_model.update_timestamp!
83
+ end
84
+ end
85
+
86
+ === Caching a single record
87
+
88
+ When finding a single record with +first_with_cache+ or +find_with_cache+
89
+
90
+ 1. The database is queried to get the id and updated at timestamp on the record
91
+ 2. The id and timestamp are used to generate a cache key and the cache is checked
92
+ 3. If the key is not found in the cache, the database will be queried again for the record by id
93
+
94
+ This will really only boost performance on records with many large columns or when you include associations to be cached with the record. You should verify that adding caching actually gives you a benefit.
95
+
96
+ === Caching multiple records
97
+
98
+ When finding multiple records with +all_with_cache+ or +with_cache+
99
+
100
+ 1. The database is queried for the maximum value in the updated at column and the count of the number of rows in the table
101
+ 2. This is used to generate a cache key and the cache is checked
102
+ 3. If the key is not found in the cache, the database query will be done and the results will be cached
103
+
104
+ This can give a pretty good performance boost especially for things like getting all the values in a small table to display in a view. If the table is updated frequently, the benefits will be reduced because any updates to the table will invalidate all the cache entries.
105
+
106
+ === MySQL Timestamp Accuracy
107
+
108
+ Since MySQL only stores datetimes with a precision of 1 second, there is a small possibility that stale data could be in the cache if a record is updated twice within one second and in between is read into the cache. All the other major databases use much higher precision on timestamps and are not affected by this issue. As a work around, you can use a FLOAT column for your timestamps instead of a DATETIME.
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'rubygems/package_task'
3
+ require 'rake'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'RVM likes to call it tests'
9
+ task :tests => :test
10
+
11
+ begin
12
+ require 'rspec'
13
+ require 'rspec/core/rake_task'
14
+ desc 'Run the unit tests'
15
+ RSpec::Core::RakeTask.new(:test)
16
+ rescue LoadError
17
+ task :test do
18
+ STDERR.puts "You must have rspec 2.0 installed to run the tests"
19
+ end
20
+ end
21
+
22
+ spec_file = File.expand_path('../last_mod_cache.gemspec', __FILE__)
23
+ if File.exist?(spec_file)
24
+ spec = eval(File.read(spec_file))
25
+
26
+ Gem::PackageTask.new(spec) do |p|
27
+ p.gem_spec = spec
28
+ end
29
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,224 @@
1
+ require 'active_record'
2
+
3
+ module LastModCache
4
+ extend ActiveSupport::Concern
5
+
6
+ DYNAMIC_FINDER_METHOD_PATTERN = /^find_(all_)?by_(.+)_with_cache$/
7
+
8
+ included do
9
+ class_eval do
10
+ class << self
11
+ alias_method_chain(:method_missing, :last_mod_cache)
12
+ end
13
+ end
14
+ class_attribute :last_mod_cache, :updated_at_column, :instance_reader => false, :instance_writer => false
15
+ self.last_mod_cache = Rails.cache if defined?(Rails)
16
+ self.updated_at_column = :updated_at
17
+
18
+ ActiveRecord::Relation.send(:include, RelationCache) unless ActiveRecord::Relation.include?(RelationCache)
19
+ end
20
+
21
+ # Module added to ActiveRecord::Relation to add the +with_cache+ method.
22
+ module RelationCache
23
+ # Add +with_cache+ to the end of a relation chain to perform the find and store the results in cache.
24
+ # Options for cache storage can be set with the optional +cache_options+ parameter. This method is
25
+ # equivalent to calling +to_a+ on the relation so that no more relations can be chained after it is called.
26
+ #
27
+ # Example:
28
+ #
29
+ # BlogPosts.where(:blog_id => my_blog.id).order("published_at DESC").limit(20).with_cache
30
+ def with_cache(cache_options = nil)
31
+ raise NotImplementedError.new("LastModCache is not available on #{klass}") unless klass.include?(LastModCache)
32
+ bind_variables = nil
33
+ if respond_to?(:bind_values)
34
+ bind_variables = bind_values.collect do |column, value|
35
+ column.type_cast(value)
36
+ end
37
+ end
38
+ klass.all_with_cache(:sql => to_sql, :cache => cache_options, :bind_values => bind_variables) do
39
+ to_a
40
+ end
41
+ end
42
+ end
43
+
44
+ # Class methods mixed into an ActiveRecord model that includes LastModCache.
45
+ module ClassMethods
46
+ # Find all records that match a query and store it in the cache. The cache entry will be invalidated whenever
47
+ # the +updated_at+ column is advanced on any record in the table.
48
+ #
49
+ # The +options+ parameter can contain any options allowed in the +all+ method with the addition of a
50
+ # <tt>:cache</tt> option which can be used to pass options to the cache itself.
51
+ def all_with_cache(options = {}, &block)
52
+ options = deep_clone(options)
53
+ cache_options, options = extract_cache_options(options)
54
+ block ||= lambda{ all(options) }
55
+ Proxy.new do
56
+ max_updated_at, count = max_updated_at_and_count
57
+ records = last_mod_cache.fetch(updated_at_cache_key(:all_with_cache, options, max_updated_at, count), cache_options, &block)
58
+ records.freeze
59
+ end
60
+ end
61
+
62
+ # Find the first that matches a query and store it in the cache. The cache entry will be invalidated whenever
63
+ # the +updated_at+ column on that record is changed.
64
+ #
65
+ # The +options+ parameter can contain any options allowed in the +first+ method with the addition of a
66
+ # <tt>:cache</tt> option which can be used to pass options to the cache itself.
67
+ def first_with_cache(options = {}, &block)
68
+ options = deep_clone(options)
69
+ cache_options, options = extract_cache_options(options)
70
+ conditions = options.delete(:conditions)
71
+ Proxy.new do
72
+ id, timestamp = id_and_updated_at(conditions)
73
+ block ||= lambda{ all(options.merge(:limit => 1, :conditions => {:id => id})).first if id }
74
+ record = last_mod_cache.fetch(updated_at_cache_key(:first_with_cache, options.merge(:conditions => conditions), timestamp), cache_options, &block)
75
+ record.freeze if record
76
+ end
77
+ end
78
+
79
+ # Find a record by id or ids in database and store it in the cache. The cache entry will be invalidated whenever
80
+ # the +updated_at+ column on that record is changed.
81
+ #
82
+ # The +options+ parameter can contain any options allowed in the +first+ method with the addition of a
83
+ # <tt>:cache</tt> option which can be used to pass options to the cache
84
+ def find_with_cache(id_or_ids, options = nil)
85
+ options = options ? deep_clone(options) : {}
86
+ cache_options, options = extract_cache_options(options)
87
+ finder = lambda{ options.blank? ? find(id_or_ids) : find(id_or_ids, options) }
88
+ if id_or_ids.is_a?(Array)
89
+ all_with_cache(options.merge(:conditions => {:id => id_or_ids}, :cache => cache_options), &finder)
90
+ else
91
+ first_with_cache(options.merge(:conditions => {:id => id_or_ids}, :cache => cache_options), &finder)
92
+ end
93
+ end
94
+
95
+ # Get the cache configuration for the class.
96
+ def last_mod_cache_config
97
+ if defined?(@last_mod_cache_config) && @last_mod_cache_config
98
+ @last_mod_cache_config
99
+ else
100
+ superclass.last_mod_cache_config if superclass.respond_to?(:last_mod_cache_config)
101
+ end
102
+ end
103
+
104
+ # Hook into method_missing to add "_with_cache" as a suffix to dynamic finder methods.
105
+ def method_missing_with_last_mod_cache(method, *args, &block) #:nodoc:
106
+ match = method.to_s.match(DYNAMIC_FINDER_METHOD_PATTERN)
107
+ if match
108
+ finder_column_names = match[2].split("_and_")
109
+ finder_values = args.dup
110
+ options = finder_values.extract_options!
111
+
112
+ unless finder_column_names.size == finder_values.size
113
+ raise ArgumentError.new("wrong number of arguments (#{finder_values.size} for #{finder_column_names.size})")
114
+ end
115
+
116
+ unless (column_names & finder_column_names).size == finder_column_names.size
117
+ raise NoMethodError.new("dynamic finder #{method} does not exist on #{name}")
118
+ end
119
+
120
+ conditions = {}
121
+ finder_column_names.zip(finder_values).each do |col, val|
122
+ conditions[col] = val
123
+ end
124
+ options = options.merge(:conditions => conditions)
125
+ match[1] ? all_with_cache(options) : first_with_cache(options)
126
+ else
127
+ method_missing_without_last_mod_cache(method, *args, &block)
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ # Construct a cache key based on a timestamp.
134
+ def updated_at_cache_key(method, options, timestamp, row_count = nil)
135
+ key = options.merge(:class => name, :method => method, :updated_at => timestamp.to_f)
136
+ key[:row_count] = row_count if row_count
137
+ key
138
+ end
139
+
140
+ # Get the maximum value in the updated at column and the count of all records in the database.
141
+ def max_updated_at_and_count
142
+ result = connection.select_one("SELECT MAX(#{connection.quote_column_name(updated_at_column)}) AS #{connection.quote_column_name('updated_at')}, COUNT(*) AS #{connection.quote_column_name('row_size')} FROM #{connection.quote_table_name(table_name)}")
143
+ updated_at = result['updated_at']
144
+ updated_at = columns_hash[updated_at_column.to_s].type_cast(updated_at) if updated_at.is_a?(String)
145
+ [updated_at, result['row_size'].to_i]
146
+ end
147
+
148
+ # Get the id and updated at value for the first row that matches the conditions.
149
+ def id_and_updated_at(conditions)
150
+ column = columns_hash[updated_at_column.to_s]
151
+ sql = "SELECT #{connection.quote_column_name(primary_key)} AS #{connection.quote_column_name('id')}, #{connection.quote_column_name(updated_at_column)} AS #{connection.quote_column_name('updated_at')} FROM #{connection.quote_table_name(table_name)}"
152
+ sql << " WHERE #{sanitize_sql_for_conditions(conditions)}" if conditions
153
+ result = connection.select_one(sql)
154
+ if result
155
+ updated_at = result['updated_at']
156
+ updated_at = columns_hash[updated_at_column.to_s].type_cast(updated_at) if updated_at.is_a?(String)
157
+ [result['id'], updated_at]
158
+ else
159
+ []
160
+ end
161
+ end
162
+
163
+ # Pull the :cache options from the options hash.
164
+ def extract_cache_options(options)
165
+ if options.include?(:cache)
166
+ options.dup
167
+ cache_options = options.delete(:cache)
168
+ [cache_options, options]
169
+ else
170
+ [{}, options]
171
+ end
172
+ end
173
+
174
+ # Create a deep clone of a hash where all values are cloned as well. This is used
175
+ # to isolate any hash values sent to a Proxy object so that the values can't be changed
176
+ # after the Proxy is created.
177
+ def deep_clone(obj)
178
+ case obj
179
+ when Hash
180
+ clone = {}
181
+ obj.each do |k, v|
182
+ clone[k] = deep_clone(v)
183
+ end
184
+ obj = clone
185
+ when Array
186
+ obj.collect{|a| deep_clone(a)}
187
+ when String
188
+ obj.clone
189
+ else
190
+ obj
191
+ end
192
+ end
193
+ end
194
+
195
+ module InstanceMethods
196
+ # Force an update to the timestamp column. This method can be invoked to force cache entries to expire.
197
+ # Validations and callbacks will *not* be called. If you need those called, simply call +update_attribute+ instead.
198
+ def update_timestamp!
199
+ col_name = self.class.updated_at_column
200
+ self.send("#{col_name}=", Time.now)
201
+ timestamp = self.send(col_name)
202
+ conn = self.class.connection
203
+ sql = self.class.send(:sanitize_sql, ["UPDATE #{conn.quote_table_name(self.class.table_name)} SET #{conn.quote_column_name(col_name)} = ? WHERE #{conn.quote_column_name(self.class.primary_key)} = ?", timestamp, id])
204
+ conn.update(sql)
205
+ end
206
+ end
207
+
208
+ # Proxy class that sends all method calls to a block.
209
+ class Proxy #:nodoc:
210
+ required_methods = {"__send__" => true, "__id__" => true}
211
+ instance_methods.each do |m|
212
+ undef_method(m) unless required_methods.include?(m.to_s)
213
+ end
214
+
215
+ def initialize(&block)
216
+ @block = block
217
+ end
218
+
219
+ def method_missing(method, *args, &block)
220
+ @object = @block.call unless defined?(@object)
221
+ @object.send(method, *args, &block)
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,608 @@
1
+ require 'spec_helper'
2
+
3
+ describe LastModCache do
4
+ before :each do
5
+ LastModCache::Test::ModelOne.delete_all
6
+ LastModCache::Test::ModelTwo.delete_all
7
+ LastModCache::Test::ModelThree.delete_all
8
+ LastModCache::Test::ModelFour.delete_all
9
+ Rails.cache.clear
10
+ LastModCache::Test.cache.clear
11
+ end
12
+
13
+ let(:model_one_record_one){LastModCache::Test::ModelOne.create!(:name => "one", :value => 1)}
14
+ let(:model_one_record_two){LastModCache::Test::ModelOne.create!(:name => "two", :value => 2)}
15
+ let(:model_one_record_three){LastModCache::Test::ModelOne.create!(:name => "three", :value => 3)}
16
+
17
+ let(:model_two_record_one){LastModCache::Test::ModelTwo.create!(:name => "one", :value => 1)}
18
+ let(:model_two_record_two){LastModCache::Test::ModelTwo.create!(:name => "two", :value => 2)}
19
+ let(:model_two_record_three){LastModCache::Test::ModelTwo.create!(:name => "three", :value => 3)}
20
+
21
+ let(:model_three_record_one){LastModCache::Test::ModelThree.create!(:name => "one", :value => 1)}
22
+ let(:model_three_record_two){LastModCache::Test::ModelThree.create!(:name => "two", :value => 2)}
23
+ let(:model_three_record_three){LastModCache::Test::ModelThree.create!(:name => "three", :value => 3)}
24
+
25
+ let(:model_four_record_one){LastModCache::Test::ModelFour.create!(:name => "one", :value => 1)}
26
+ let(:model_four_record_two){LastModCache::Test::ModelFour.create!(:name => "two", :value => 2)}
27
+ let(:model_four_record_three){LastModCache::Test::ModelFour.create!(:name => "three", :value => 3)}
28
+
29
+ context "configuration" do
30
+ it "should use Rails.cache by default" do
31
+ LastModCache::Test::ModelOne.last_mod_cache.should == Rails.cache
32
+ end
33
+
34
+ it "should be able to set the cache" do
35
+ LastModCache::Test::ModelTwo.last_mod_cache.should == LastModCache::Test.cache
36
+ end
37
+
38
+ it "should inherit the cache from a parent class" do
39
+ LastModCache::Test::ModelThree.last_mod_cache.should == LastModCache::Test.cache
40
+ end
41
+
42
+ it "should use update_at as the default updated at column" do
43
+ LastModCache::Test::ModelOne.updated_at_column.should == :updated_at
44
+ end
45
+
46
+ it "should be able to set the updated at column" do
47
+ LastModCache::Test::ModelTwo.updated_at_column.should == :modified_at
48
+ end
49
+
50
+ it "should inherit the updated at column from a parent class" do
51
+ LastModCache::Test::ModelThree.updated_at_column.should == :modified_at
52
+ end
53
+ end
54
+
55
+ context "updating the timestamp" do
56
+ it "should update" do
57
+ t = model_one_record_one.updated_at
58
+ model_one_record_one.update_timestamp!
59
+ model_one_record_one.updated_at.should > t
60
+ model_one_record_one.reload
61
+ model_one_record_one.updated_at.should > t
62
+ end
63
+
64
+ it "should update when using a custom update at column" do
65
+ t = model_two_record_one.modified_at
66
+ model_two_record_one.update_timestamp!
67
+ model_two_record_one.modified_at.should > t
68
+ model_two_record_one.reload
69
+ model_two_record_one.modified_at.should > t
70
+ end
71
+
72
+ it "should update when using a non-datetime column" do
73
+ t = model_four_record_one.last_modified
74
+ model_four_record_one.update_timestamp!
75
+ model_four_record_one.last_modified.should > t
76
+ model_four_record_one.reload
77
+ model_four_record_one.last_modified.should > t
78
+ end
79
+ end
80
+
81
+ context "find all" do
82
+ before :each do
83
+ model_one_record_one
84
+ model_one_record_two
85
+ model_one_record_three
86
+ end
87
+
88
+ it "should find all records with a query and put them in the cache" do
89
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:name => ["one", "two"]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
90
+ LastModCache::Test::ModelOne.all_with_cache(:conditions => {:name => ["one", "two"]}).should == [model_one_record_one, model_one_record_two]
91
+ Rails.cache.read(cache_key).should == [model_one_record_one, model_one_record_two]
92
+ end
93
+
94
+ it "should find all records with a query from the cache" do
95
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:name => ["one", "two"]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
96
+ Rails.cache.write(cache_key, [model_one_record_one, model_one_record_two])
97
+ LastModCache::Test::ModelOne.poke_column_value(model_one_record_two.id, :value, 0)
98
+ LastModCache::Test::ModelOne.all_with_cache(:conditions => {:name => ["one", "two"]}).collect{|r| r.value}.should == [1, 2]
99
+ end
100
+
101
+ it "should pass :cache option through to the cache" do
102
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:name => ["two"]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
103
+ Rails.cache.write(cache_key, [model_one_record_two], :namespace => "test")
104
+ LastModCache::Test::ModelOne.poke_column_value(model_one_record_two.id, :value, 0)
105
+ LastModCache::Test::ModelOne.all_with_cache(:cache => {:namespace => "test"}, :conditions => {:name => ["two"]}).collect{|r| r.value}.should == [2]
106
+ end
107
+
108
+ it "should invalidate a find all cache whenever any record is modified" do
109
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:name => ["one", "two"]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
110
+ Rails.cache.write(cache_key, [model_one_record_one, model_one_record_two])
111
+ LastModCache::Test::ModelOne.poke_column_value(model_one_record_two.id, :value, 0)
112
+ model_one_record_three.update_attribute(:value, 4)
113
+ LastModCache::Test::ModelOne.all_with_cache(:conditions => {:name => ["one", "two"]}).collect{|r| r.value}.should == [1, 0]
114
+ end
115
+
116
+ it "should invalidate a find all cache whenever any record is deleted" do
117
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:name => ["two", "three"]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
118
+ Rails.cache.write(cache_key, [model_one_record_one, model_one_record_two])
119
+ LastModCache::Test::ModelOne.poke_column_value(model_one_record_two.id, :value, 0)
120
+ model_one_record_one.destroy
121
+ LastModCache::Test::ModelOne.all_with_cache(:conditions => {:name => ["two", "three"]}).collect{|r| r.value}.should == [0, 3]
122
+ end
123
+
124
+ it "should work with models that don't use the defaults" do
125
+ model_two_record_one
126
+ model_two_record_two
127
+ model_two_record_three
128
+
129
+ timestamp = Time.now - 60
130
+ LastModCache::Test::ModelTwo.poke_column_value(model_two_record_two.id, :modified_at, timestamp)
131
+ model_two_record_two.reload
132
+ LastModCache::Test::ModelTwo.all_with_cache(:conditions => {:name => "two"}).collect{|r| r.value}.should == [2]
133
+
134
+ cache_key = {:class => "LastModCache::Test::ModelTwo", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelTwo.maximum(:modified_at).to_f, :conditions => {:name => "two"}, :row_count => 3}
135
+ LastModCache::Test.cache.write(cache_key, [model_two_record_two.dup])
136
+ LastModCache::Test::ModelTwo.poke_column_value(model_two_record_two.id, :value, 0)
137
+ LastModCache::Test::ModelTwo.all_with_cache(:conditions => {:name => "two"}).collect{|r| r.value}.should == [2]
138
+
139
+ model_two_record_two.update_attribute(:value, 5)
140
+ model_two_record_two.reload
141
+ LastModCache::Test::ModelTwo.all_with_cache(:conditions => {:name => "two"}).collect{|r| r.value}.should == [5]
142
+ LastModCache::Test.cache.read(cache_key.merge(:updated_at => LastModCache::Test::ModelTwo.maximum(:modified_at).to_f)).should == [model_two_record_two]
143
+ end
144
+
145
+ it "should work with a numeric timestamp column" do
146
+ model_four_record_one
147
+ model_four_record_two
148
+ model_four_record_three
149
+
150
+ cache_key = {:class => "LastModCache::Test::ModelFour", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelFour.maximum(:last_modified), :conditions => {:name => "two"}, :row_count => 3}
151
+ Rails.cache.write(cache_key, [model_four_record_two.dup])
152
+ LastModCache::Test::ModelFour.poke_column_value(model_four_record_two.id, :value, 0)
153
+ LastModCache::Test::ModelFour.all_with_cache(:conditions => {:name => "two"}).collect{|r| r.value}.should == [2]
154
+
155
+ model_four_record_two.update_attribute(:value, 5)
156
+ model_four_record_two.reload
157
+ LastModCache::Test::ModelFour.all_with_cache(:conditions => {:name => "two"}).collect{|r| r.value}.should == [5]
158
+ Rails.cache.read(cache_key.merge(:updated_at => LastModCache::Test::ModelFour.maximum(:last_modified).to_f)).should == [model_four_record_two]
159
+ end
160
+
161
+ it "should be lazy loaded" do
162
+ Rails.cache.should_not_receive(:read)
163
+ Rails.cache.should_not_receive(:write)
164
+ LastModCache::Test::ModelOne.should_not_receive(:connection)
165
+ LastModCache::Test::ModelOne.all_with_cache(:conditions => {:name => ["one", "two"]})
166
+ nil
167
+ end
168
+
169
+ it "should always return a frozen object" do
170
+ LastModCache::Test::ModelOne.all_with_cache(:conditions => {:name => "two"}).should be_frozen
171
+ LastModCache::Test::ModelOne.all_with_cache(:conditions => {:name => "four"}).should be_frozen
172
+ end
173
+
174
+ it "should not require any options" do
175
+ LastModCache::Test::ModelOne.all_with_cache.should == LastModCache::Test::ModelOne.all
176
+ end
177
+ end
178
+
179
+ context "find one by query" do
180
+ before :each do
181
+ model_one_record_one
182
+ model_one_record_two
183
+ model_one_record_three
184
+ end
185
+
186
+ it "should find a single record with a query and put it in the cache" do
187
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :updated_at => model_one_record_two.updated_at.to_f, :conditions => {:name => "two"}}
188
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "two"}).should == model_one_record_two
189
+ Rails.cache.read(cache_key).should == model_one_record_two
190
+ end
191
+
192
+ it "should find a single record with a query from the cache" do
193
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :updated_at => model_one_record_two.updated_at.to_f, :conditions => {:name => "two"}}
194
+ Rails.cache.write(cache_key, model_one_record_two)
195
+ LastModCache::Test::ModelOne.poke_column_value(model_one_record_two.id, :value, 0)
196
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "two"}).value.should == 2
197
+ end
198
+
199
+ it "should invalidate a single record query cache entry when the record is modified" do
200
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :updated_at => model_one_record_two.updated_at.to_f, :conditions => {:name => "two"}}
201
+ Rails.cache.write(cache_key, model_one_record_two)
202
+ model_one_record_two.update_attribute(:value, 0)
203
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "two"}).value.should == 0
204
+ end
205
+
206
+ it "should not find a record by query if the updated time for the record could not be found" do
207
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "not found"}).should == nil
208
+ end
209
+
210
+ it "should not find a record by query if it has been deleted since it was cached" do
211
+ model_one_record_two
212
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "two"}).should == model_one_record_two
213
+ model_one_record_two.destroy
214
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "two"}).should == nil
215
+ end
216
+
217
+ it "should work with models that don't use the defaults" do
218
+ model_four_record_one
219
+ model_four_record_two
220
+ model_four_record_three
221
+
222
+ timestamp = Time.now - 60
223
+ LastModCache::Test::ModelTwo.poke_column_value(model_two_record_two.id, :modified_at, timestamp)
224
+ model_two_record_two.reload
225
+ LastModCache::Test::ModelTwo.first_with_cache(:conditions => {:name => "two"}).value.should == 2
226
+
227
+ cache_key = {:class => "LastModCache::Test::ModelTwo", :method => :first_with_cache, :updated_at => timestamp.to_f, :conditions => {:name => "two"}}
228
+ LastModCache::Test.cache.write(cache_key, model_two_record_two.dup)
229
+ LastModCache::Test::ModelTwo.poke_column_value(model_two_record_two.id, :value, 0)
230
+ LastModCache::Test::ModelTwo.first_with_cache(:conditions => {:name => "two"}).value.should == 2
231
+
232
+ model_two_record_two.update_attribute(:value, 5)
233
+ model_two_record_two.reload
234
+ LastModCache::Test::ModelTwo.first_with_cache(:conditions => {:name => "two"}).value.should == 5
235
+ LastModCache::Test.cache.read(cache_key.merge(:updated_at => model_two_record_two.modified_at.to_f)).should == model_two_record_two
236
+ end
237
+
238
+ it "should work with a numeric timestamp column" do
239
+ model_four_record_one
240
+ model_four_record_two
241
+ model_four_record_three
242
+
243
+ cache_key = {:class => "LastModCache::Test::ModelFour", :method => :first_with_cache, :updated_at => model_four_record_two.last_modified, :conditions => {:name => "two"}}
244
+ Rails.cache.write(cache_key, model_four_record_two.dup)
245
+ LastModCache::Test::ModelFour.poke_column_value(model_four_record_two.id, :value, 0)
246
+ LastModCache::Test::ModelFour.first_with_cache(:conditions => {:name => "two"}).value.should == 2
247
+
248
+ model_four_record_two.update_attribute(:value, 5)
249
+ model_four_record_two.reload
250
+ LastModCache::Test::ModelFour.first_with_cache(:conditions => {:name => "two"}).value.should == 5
251
+ Rails.cache.read(cache_key.merge(:updated_at => model_four_record_two.last_modified)).should == model_four_record_two
252
+ end
253
+
254
+ it "should be lazy loaded" do
255
+ Rails.cache.should_not_receive(:read)
256
+ Rails.cache.should_not_receive(:write)
257
+ LastModCache::Test::ModelOne.should_not_receive(:connection)
258
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "two"})
259
+ nil
260
+ end
261
+
262
+ it "should always return a frozen object or nil" do
263
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "two"}).should be_frozen
264
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "four"}).should == nil
265
+ end
266
+
267
+ it "should not require any options" do
268
+ LastModCache::Test::ModelOne.first_with_cache.should == LastModCache::Test::ModelOne.first
269
+ end
270
+ end
271
+
272
+ context "find by id" do
273
+ before :each do
274
+ model_one_record_one
275
+ model_one_record_two
276
+ model_one_record_three
277
+ end
278
+
279
+ it "should find a single record by id and put it in the cache" do
280
+ model_one_record_one
281
+ model_one_record_two
282
+ model_one_record_three
283
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :conditions => {:id => model_one_record_two.id}, :updated_at => model_one_record_two.updated_at.to_f}
284
+ LastModCache::Test::ModelOne.find_with_cache(model_one_record_two.id).should == model_one_record_two
285
+ Rails.cache.read(cache_key).should == model_one_record_two
286
+ end
287
+
288
+ it "should find a single record by id from the cache" do
289
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :conditions => {:id => model_one_record_two.id}, :updated_at => model_one_record_two.updated_at.to_f}
290
+ Rails.cache.write(cache_key, model_one_record_two)
291
+ LastModCache::Test::ModelOne.poke_column_value(model_one_record_two.id, :value, 0)
292
+ LastModCache::Test::ModelOne.find_with_cache(model_one_record_two.id).value.should == 2
293
+ end
294
+
295
+ it "should invalidate a single record by id cache entry when the record is modified" do
296
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :conditions => {:id => model_one_record_two.id}, :updated_at => model_one_record_two.updated_at.to_f}
297
+ Rails.cache.write(cache_key, model_one_record_two)
298
+ model_one_record_two.update_attribute(:value, 0)
299
+ LastModCache::Test::ModelOne.find_with_cache(model_one_record_two.id).value.should == 0
300
+ end
301
+
302
+ it "should find multiple records by id and put them in the cache" do
303
+ model_one_record_one
304
+ model_one_record_two
305
+ model_one_record_three
306
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:id => [model_one_record_one.id, model_one_record_two.id]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
307
+ LastModCache::Test::ModelOne.find_with_cache([model_one_record_one.id, model_one_record_two.id]).should == [model_one_record_one, model_one_record_two]
308
+ Rails.cache.read(cache_key).should == [model_one_record_one, model_one_record_two]
309
+ end
310
+
311
+ it "should find multiple records by id from the cache" do
312
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:id => [model_one_record_one.id, model_one_record_two.id]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
313
+ Rails.cache.write(cache_key, [model_one_record_one, model_one_record_two])
314
+ LastModCache::Test::ModelOne.poke_column_value(model_one_record_one.id, :value, 0)
315
+ LastModCache::Test::ModelOne.find_with_cache([model_one_record_one.id, model_one_record_two.id]).first.value.should == 1
316
+ end
317
+
318
+ it "should invalidate a multiple records by id cache entry when any record is modified" do
319
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:id => [model_one_record_one.id, model_one_record_two.id]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
320
+ Rails.cache.write(cache_key, [model_one_record_one, model_one_record_two])
321
+ model_one_record_one.update_attribute(:value, 0)
322
+ LastModCache::Test::ModelOne.find_with_cache([model_one_record_one.id, model_one_record_two.id]).first.value.should == 0
323
+ end
324
+
325
+ it "should invalidate a multiple records by id cache entry when any record is deleted" do
326
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:id => [model_one_record_one.id, model_one_record_three.id]}, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
327
+ Rails.cache.write(cache_key, [model_one_record_one, model_one_record_three])
328
+
329
+ LastModCache::Test::ModelOne.poke_column_value(model_one_record_one.id, :value, 0)
330
+ model_one_record_two.destroy
331
+ LastModCache::Test::ModelOne.find_with_cache([model_one_record_one.id, model_one_record_three.id]).first.value.should == 0
332
+ end
333
+
334
+ it "should raise a RecordNotFound error if a record could not be found" do
335
+ lambda{LastModCache::Test::ModelOne.find_with_cache(-1).nil?}.should raise_error(ActiveRecord::RecordNotFound)
336
+ end
337
+
338
+ it "should raise a RecordNotFound error if any of the records can't be found" do
339
+ lambda{LastModCache::Test::ModelOne.find_with_cache([model_one_record_one.id, -1]).nil?}.should raise_error(ActiveRecord::RecordNotFound)
340
+ end
341
+
342
+ it "should raise a RecordNotFound error if the record has been deleted since it was cached" do
343
+ model_one_record_two
344
+ LastModCache::Test::ModelOne.find_with_cache(model_one_record_two.id).should == model_one_record_two
345
+ model_one_record_two.destroy
346
+ lambda{LastModCache::Test::ModelOne.find_with_cache(model_one_record_two.id).nil?}.should raise_error(ActiveRecord::RecordNotFound)
347
+ end
348
+
349
+ it "should work with models that don't use the defaults" do
350
+ model_four_record_one
351
+ model_four_record_two
352
+ model_four_record_three
353
+
354
+ timestamp = Time.now - 60
355
+ LastModCache::Test::ModelTwo.poke_column_value(model_two_record_two.id, :modified_at, timestamp)
356
+ model_one_record_two.reload
357
+ LastModCache::Test::ModelTwo.find_with_cache(model_two_record_two.id).value.should == 2
358
+
359
+ cache_key = {:class => "LastModCache::Test::ModelTwo", :method => :first_with_cache, :conditions => {:id => model_two_record_two.id}, :updated_at => model_one_record_two.updated_at.to_f}
360
+ LastModCache::Test.cache.write(cache_key, model_two_record_two.dup)
361
+ LastModCache::Test::ModelTwo.poke_column_value(model_two_record_two.id, :value, 0)
362
+ LastModCache::Test::ModelTwo.find_with_cache(model_two_record_two.id).value.should == 2
363
+
364
+ model_two_record_two.update_attribute(:value, 5)
365
+ model_two_record_two.reload
366
+ LastModCache::Test::ModelTwo.find_with_cache(model_two_record_two.id).value.should == 5
367
+ LastModCache::Test.cache.read(cache_key.merge(:updated_at => model_two_record_two.modified_at.to_f)).should == model_two_record_two
368
+ end
369
+
370
+ it "should be lazy loaded" do
371
+ Rails.cache.should_not_receive(:read)
372
+ Rails.cache.should_not_receive(:write)
373
+ LastModCache::Test::ModelOne.should_not_receive(:connection)
374
+ LastModCache::Test::ModelOne.find_with_cache(model_one_record_two.id)
375
+ LastModCache::Test::ModelOne.find_with_cache([model_one_record_one.id, model_one_record_two.id])
376
+ nil
377
+ end
378
+ end
379
+
380
+ context "dynamic query methods" do
381
+ before :each do
382
+ model_one_record_one
383
+ model_one_record_two
384
+ model_one_record_three
385
+ end
386
+
387
+ it "should pass dynamic find methods through the cache" do
388
+ relation = LastModCache::Test::ModelOne.where(:name => "two").limit(1)
389
+ LastModCache::Test::ModelOne.find_by_name_with_cache("two").should == model_one_record_two
390
+ timestamp = model_one_record_two.updated_at
391
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :updated_at => timestamp.to_f, :conditions => {"name" => "two"}}
392
+ Rails.cache.read(cache_key).should == model_one_record_two
393
+ end
394
+
395
+ it "should pass dynamic find_all methods through the cache" do
396
+ LastModCache::Test::ModelOne.find_all_by_name_with_cache("two").should == [model_one_record_two]
397
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3, :conditions => {"name" => "two"}}
398
+ Rails.cache.read(cache_key).should == [model_one_record_two]
399
+ end
400
+
401
+ it "should pass dynamic find methods with multiple conditions through the cache" do
402
+ model_one_record_one.update_attribute(:name, "two")
403
+ model_one_record_three.update_attribute(:name, "two")
404
+ LastModCache::Test::ModelOne.find_by_name_and_value_with_cache("two", 2).should == model_one_record_two
405
+ timestamp = model_one_record_two.updated_at
406
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :updated_at => timestamp.to_f, :conditions => {"name" => "two", "value" => 2}}
407
+ Rails.cache.read(cache_key).should == model_one_record_two
408
+ end
409
+
410
+ it "should pass dynamic find_all methods with multiple conditions through the cache" do
411
+ model_one_record_one.update_attribute(:name, "two")
412
+ model_one_record_three.update_attribute(:name, "two")
413
+ LastModCache::Test::ModelOne.find_all_by_name_and_value_with_cache("two", 2).should == [model_one_record_two]
414
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3, :conditions => {"name" => "two", "value" => 2}}
415
+ Rails.cache.read(cache_key).should == [model_one_record_two]
416
+ end
417
+
418
+ it "should not interfere with normal method missing behavior" do
419
+ LastModCache::Test::ModelOne.find_by_name("one").should == model_one_record_one
420
+ LastModCache::Test::ModelOne.find_by_name_and_value("one", 1).should == model_one_record_one
421
+ lambda{ LastModCache::Test::ModelOne.this_is_not_a_method }.should raise_error(NoMethodError)
422
+ end
423
+
424
+ it "should raise an error when the wrong number of arguments are passed" do
425
+ lambda{ LastModCache::Test::ModelOne.find_by_name_with_cache("one", 1) }.should raise_error(ArgumentError)
426
+ lambda{ LastModCache::Test::ModelOne.find_by_name_with_cache }.should raise_error(ArgumentError)
427
+ end
428
+
429
+ it "should raise an error if a column doesn not exist" do
430
+ lambda{ LastModCache::Test::ModelOne.find_by_fleevium_with_cache("bloork") }.should raise_error(NoMethodError)
431
+ lambda{ LastModCache::Test::ModelOne.find_by_name_and_stuff_with_cache("one", :x) }.should raise_error(NoMethodError)
432
+ end
433
+
434
+ it "should be lazy loaded" do
435
+ Rails.cache.should_not_receive(:read)
436
+ Rails.cache.should_not_receive(:write)
437
+ LastModCache::Test::ModelOne.should_not_receive(:connection)
438
+ LastModCache::Test::ModelOne.find_by_name_and_value_with_cache("one", 1)
439
+ LastModCache::Test::ModelOne.find_all_by_name_with_cache("one")
440
+ nil
441
+ end
442
+ end
443
+
444
+ context ActiveRecord::Relation do
445
+ before :each do
446
+ model_one_record_one
447
+ model_one_record_two
448
+ model_one_record_three
449
+ end
450
+
451
+ it "should cache the result of a relation chain" do
452
+ relation = LastModCache::Test::ModelOne.where(:name => ["one", "two"]).order("value DESC")
453
+ relation.with_cache.should == [model_one_record_two, model_one_record_one]
454
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3, :sql => relation.to_sql, :bind_values => nil}
455
+ Rails.cache.read(cache_key).should == [model_one_record_two, model_one_record_one]
456
+ Rails.cache.write(cache_key, [model_one_record_one, model_one_record_three])
457
+ relation.with_cache.should == [model_one_record_one, model_one_record_three]
458
+ end
459
+
460
+ it "should cache the result using bind variables if supported" do
461
+ name_column = LastModCache::Test::ModelOne.columns_hash["name"]
462
+ relation = LastModCache::Test::ModelOne.where("name IN (?, ?)")
463
+ if relation.respond_to?(:bind_values)
464
+ bind_relation = relation.bind([name_column, "one"]).bind([name_column, "two"]).order("value DESC")
465
+ bind_relation.with_cache.should == [model_one_record_two, model_one_record_one]
466
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3, :sql => bind_relation.to_sql, :bind_values => ["one", "two"]}
467
+ Rails.cache.read(cache_key).should == [model_one_record_two, model_one_record_one]
468
+ Rails.cache.write(cache_key, [model_one_record_one, model_one_record_three])
469
+ bind_relation.with_cache.should == [model_one_record_one, model_one_record_three]
470
+ else
471
+ pending("ActiveRecord #{ActiveRecord::VERSION::STRING} does not support bind variables")
472
+ end
473
+ end
474
+
475
+ it "should be lazy loaded" do
476
+ Rails.cache.should_not_receive(:read)
477
+ Rails.cache.should_not_receive(:write)
478
+ LastModCache::Test::ModelOne.should_not_receive(:connection)
479
+ LastModCache::Test::ModelOne.where(:name => "one").with_cache
480
+ nil
481
+ end
482
+ end
483
+
484
+ context "eager load associations" do
485
+ before :each do
486
+ model_one_record_one.widget = LastModCache::Test::Widget.create!(:name => "widget_1")
487
+ model_one_record_one.things.create(:name => "thing_1")
488
+ model_one_record_one.things.create(:name => "thing_2")
489
+ model_one_record_one.save!
490
+ model_one_record_one.reload
491
+ model_one_record_two
492
+ model_one_record_three
493
+ end
494
+
495
+ let(:includes){ [:widget, {:things => :widget}] }
496
+
497
+ it "should cache included associations when finding many records" do
498
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :conditions => {:name => "one"}, :include => includes, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3}
499
+ LastModCache::Test::ModelOne.all_with_cache(:conditions => {:name => "one"}, :include => includes).should == [model_one_record_one]
500
+ cached = Rails.cache.read(cache_key).first
501
+ if cached.respond_to?(:association)
502
+ cached.association(:widget).loaded?.should == true
503
+ cached.association(:things).loaded?.should == true
504
+ cached.things.first.association(:widget).loaded?.should == true
505
+ else
506
+ cached.widget.loaded?.should == true
507
+ cached.things.loaded?.should == true
508
+ end
509
+ end
510
+
511
+ it "should cache included associations when finding one record" do
512
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :conditions => {:name => "one"}, :include => includes, :updated_at => model_one_record_one.updated_at.to_f}
513
+ LastModCache::Test::ModelOne.first_with_cache(:conditions => {:name => "one"}, :include => includes).should == model_one_record_one
514
+ cached = Rails.cache.read(cache_key)
515
+ if cached.respond_to?(:association)
516
+ cached.association(:widget).loaded?.should == true
517
+ cached.association(:things).loaded?.should == true
518
+ cached.things.first.association(:widget).loaded?.should == true
519
+ else
520
+ cached.widget.loaded?.should == true
521
+ cached.things.loaded?.should == true
522
+ end
523
+ end
524
+
525
+ it "should cache included associations when finding a record by id" do
526
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :first_with_cache, :conditions => {:id => model_one_record_one.id}, :include => includes, :updated_at => model_one_record_one.updated_at.to_f}
527
+ LastModCache::Test::ModelOne.find_with_cache(model_one_record_one.id, :include => includes).should == model_one_record_one
528
+ cached = Rails.cache.read(cache_key)
529
+ if cached.respond_to?(:association)
530
+ cached.association(:widget).loaded?.should == true
531
+ cached.association(:things).loaded?.should == true
532
+ cached.things.first.association(:widget).loaded?.should == true
533
+ else
534
+ cached.widget.loaded?.should == true
535
+ cached.things.loaded?.should == true
536
+ end
537
+ end
538
+
539
+ it "should cache included associations when finding with a Relation" do
540
+ relation = LastModCache::Test::ModelOne.where(:name => "one").includes(includes)
541
+ relation.with_cache.should == [model_one_record_one]
542
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3, :sql => relation.to_sql, :bind_values => nil}
543
+ cached = Rails.cache.read(cache_key).first
544
+ if cached.respond_to?(:association)
545
+ cached.association(:widget).loaded?.should == true
546
+ cached.association(:things).loaded?.should == true
547
+ cached.things.first.association(:widget).loaded?.should == true
548
+ else
549
+ cached.widget.loaded?.should == true
550
+ cached.things.loaded?.should == true
551
+ end
552
+ end
553
+
554
+ it "should cache eager load associations when finding with a Relation" do
555
+ relation = LastModCache::Test::ModelOne.where(:name => "one").eager_load(includes)
556
+ relation.with_cache.should == [model_one_record_one]
557
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3, :sql => relation.to_sql, :bind_values => nil}
558
+ cached = Rails.cache.read(cache_key).first
559
+ if cached.respond_to?(:association)
560
+ cached.association(:widget).loaded?.should == true
561
+ cached.association(:things).loaded?.should == true
562
+ cached.things.first.association(:widget).loaded?.should == true
563
+ else
564
+ cached.widget.loaded?.should == true
565
+ cached.things.loaded?.should == true
566
+ end
567
+ end
568
+
569
+ it "should cache preload associations when finding with a Relation" do
570
+ relation = LastModCache::Test::ModelOne.where(:name => "one").preload(includes)
571
+ relation.with_cache.should == [model_one_record_one]
572
+ cache_key = {:class => "LastModCache::Test::ModelOne", :method => :all_with_cache, :updated_at => LastModCache::Test::ModelOne.maximum(:updated_at).to_f, :row_count => 3, :sql => relation.to_sql, :bind_values => nil}
573
+ cached = Rails.cache.read(cache_key).first
574
+ if cached.respond_to?(:association)
575
+ cached.association(:widget).loaded?.should == true
576
+ cached.association(:things).loaded?.should == true
577
+ cached.things.first.association(:widget).loaded?.should == true
578
+ else
579
+ cached.widget.loaded?.should == true
580
+ cached.things.loaded?.should == true
581
+ end
582
+ end
583
+ end
584
+
585
+ context LastModCache::Proxy do
586
+ it "should proxy all methods except __id__" do
587
+ proxy = LastModCache::Proxy.new{ nil }
588
+ proxy.nil?.should == true
589
+ proxy.object_id.should == nil.object_id
590
+ proxy.send(:nil?).should == true
591
+ proxy.__id__.should_not == nil.object_id
592
+ end
593
+
594
+ it "should only evaluate the block once" do
595
+ proxy = LastModCache::Proxy.new{ Object.new }
596
+ proxy.object_id.should == proxy.object_id
597
+ end
598
+
599
+ it "should lazy evaluate the block" do
600
+ proxy = LastModCache::Proxy.new{ raise "never get here" }
601
+ end
602
+
603
+ it "should handle missing methods" do
604
+ proxy = LastModCache::Proxy.new{ Object.new }
605
+ lambda{ proxy.not_a_method }.should raise_error(NoMethodError)
606
+ end
607
+ end
608
+ end
@@ -0,0 +1,135 @@
1
+ require 'sqlite3'
2
+
3
+ active_record_version = ENV["ACTIVE_RECORD_VERSION"] || [">= 3.0.0"]
4
+ active_record_version = [active_record_version] unless active_record_version.is_a?(Array)
5
+ gem 'activerecord', *active_record_version
6
+
7
+ require File.expand_path("../../lib/last_mod_cache.rb", __FILE__)
8
+
9
+ puts "Testing with activerecord #{ActiveRecord::VERSION::STRING}"
10
+
11
+ module Rails
12
+ def self.cache
13
+ unless defined?(@cache)
14
+ @cache = ActiveSupport::Cache::MemoryStore.new
15
+ end
16
+ @cache
17
+ end
18
+ end
19
+
20
+ module LastModCache
21
+ module Test
22
+ class << self
23
+ def setup
24
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
25
+ Thing.setup
26
+ Widget.setup
27
+ ModelOne.setup
28
+ ModelTwo.setup
29
+ ModelFour.setup
30
+ end
31
+
32
+ def cache
33
+ unless defined?(@cache)
34
+ @cache = ActiveSupport::Cache::MemoryStore.new
35
+ end
36
+ @cache
37
+ end
38
+ end
39
+
40
+ module PokeRecordValue
41
+ def poke_column_value(id, column, value)
42
+ sql = ["UPDATE #{connection.quote_table_name(table_name)} SET #{connection.quote_column_name(column)} = ? WHERE id = ?", value, id]
43
+ connection.update(sanitize_sql_array(sql))
44
+ end
45
+ end
46
+
47
+ class Thing < ActiveRecord::Base
48
+ belongs_to :widget
49
+
50
+ class << self
51
+ def setup
52
+ connection.create_table(table_name) do |t|
53
+ t.string :name
54
+ t.integer :model_one_id
55
+ t.integer :widget_id
56
+ t.datetime :updated_at
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ class Widget < ActiveRecord::Base
63
+ class << self
64
+ def setup
65
+ connection.create_table(table_name) do |t|
66
+ t.string :name
67
+ t.datetime :updated_at
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ class ModelOne < ActiveRecord::Base
74
+ extend PokeRecordValue
75
+ include LastModCache
76
+
77
+ has_many :things
78
+ belongs_to :widget
79
+
80
+ class << self
81
+ def setup
82
+ connection.create_table(table_name) do |t|
83
+ t.string :name
84
+ t.integer :value
85
+ t.integer :widget_id
86
+ t.datetime :updated_at
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ class ModelTwo < ActiveRecord::Base
93
+ extend PokeRecordValue
94
+ include LastModCache
95
+ self.updated_at_column = :modified_at
96
+ self.last_mod_cache = Test.cache
97
+
98
+ before_save{|r| r.modified_at = Time.now}
99
+
100
+ class << self
101
+ def setup
102
+ connection.create_table(table_name) do |t|
103
+ t.string :name
104
+ t.integer :value
105
+ t.string :type
106
+ t.datetime :modified_at
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ class ModelThree < ModelTwo
113
+ end
114
+
115
+ class ModelFour < ActiveRecord::Base
116
+ extend PokeRecordValue
117
+ include LastModCache
118
+ self.updated_at_column = :last_modified
119
+
120
+ before_save{|r| r.last_modified = Time.now.to_f}
121
+
122
+ class << self
123
+ def setup
124
+ connection.create_table(table_name) do |t|
125
+ t.string :name
126
+ t.integer :value
127
+ t.float :last_modified
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ LastModCache::Test.setup
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: last_mod_cache
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Brian Durand
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-05 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 3
32
+ - 0
33
+ - 0
34
+ version: 3.0.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">"
44
+ - !ruby/object:Gem::Version
45
+ hash: 15
46
+ segments:
47
+ - 2
48
+ - 0
49
+ - 0
50
+ version: 2.0.0
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: sqlite3
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ type: :development
66
+ version_requirements: *id003
67
+ description: An extension for ActiveRecord models that adds a a very easy to use caching layer for models that include an updated at column.
68
+ email:
69
+ - bdurand@embellishedvisions.com
70
+ executables: []
71
+
72
+ extensions: []
73
+
74
+ extra_rdoc_files:
75
+ - README.rdoc
76
+ - MIT_LICENSE
77
+ files:
78
+ - README.rdoc
79
+ - VERSION
80
+ - Rakefile
81
+ - MIT_LICENSE
82
+ - lib/last_mod_cache.rb
83
+ - spec/last_mod_cache_spec.rb
84
+ - spec/spec_helper.rb
85
+ has_rdoc: true
86
+ homepage: http://github.com/bdurand/last_mod_cache
87
+ licenses: []
88
+
89
+ post_install_message:
90
+ rdoc_options:
91
+ - --charset=UTF-8
92
+ - --main
93
+ - README.rdoc
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ hash: 3
102
+ segments:
103
+ - 0
104
+ version: "0"
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ hash: 3
111
+ segments:
112
+ - 0
113
+ version: "0"
114
+ requirements: []
115
+
116
+ rubyforge_project:
117
+ rubygems_version: 1.5.2
118
+ signing_key:
119
+ specification_version: 3
120
+ summary: An extension for ActiveRecord models that adds a very easy to use caching layer for models that include an updated at column.
121
+ test_files: []
122
+