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 +20 -0
- data/README.rdoc +108 -0
- data/Rakefile +29 -0
- data/VERSION +1 -0
- data/lib/last_mod_cache.rb +224 -0
- data/spec/last_mod_cache_spec.rb +608 -0
- data/spec/spec_helper.rb +135 -0
- metadata +122 -0
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|