arid_cache 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,10 +1,50 @@
1
1
  == ARID Cache
2
2
 
3
- ARID Cache is a DRY ActiveRecord ID cache.
3
+ ARID Cache makes caching easy and effective. ARID cache supports caching on all your model named scopes, class methods and instance methods right out of the box. ARID cache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
4
4
 
5
- == Example
5
+ ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
6
6
 
7
- Given the following model:
7
+ === Counts for free
8
+
9
+ ARID Cache gives you counts for free. When a large collection is stored in the cache
10
+ ARID Cache stores the count as well so the next time you want request the count it
11
+ just takes a single read from the cache. This is also supported for your non-ActiveRecord
12
+ collections if the collection <tt>responds_to?(:count)</tt>.
13
+
14
+ Given that we have a cache like <tt>album.cached_tracks</tt> we can get the count by calling <tt>album.cached_tracks_count</tt>.
15
+
16
+ === Auto-expiring cache keys
17
+
18
+ Caches on model instances automatically incorporate the ActiveRecord <tt>cache_key</tt> which includes the <tt>updated_at</tt> timestamp of that instance, making them auto-expire when the instance is updated.
19
+
20
+ Caches on your model classes (like on the results of named scopes) will not expire however.
21
+
22
+ ARID cache provides methods to help you expire your caches.
23
+
24
+ AridCache.clear_all_caches => expires all ARID Cache caches
25
+ Model.clear_all_caches => expires class and instance-level caches for this model
26
+ Model.clear_instance_caches => expires instance-level caches for this model
27
+ Model.clear_class_caches => expires class-level caches for this model
28
+
29
+ These methods are also available on model instances.
30
+
31
+ ARID Cache keys are based on the method you call to create the cache. For example:
32
+ Album.cached_featured_albums => cache key is arid-cache-album-featured_albums
33
+ album.cached_top_tracks => cache key like arid-cache-albums/2-20090930200-top_tracks
34
+
35
+ === Support for pagination and options to <tt>find</tt>
36
+
37
+ ARID cache performs pagination and applies <tt>:limit</tt> and <tt>:offset</tt> to the IDs in memory and only selects the page/sub-set from the database, directly from the target table.
38
+
39
+ You can pass options like <tt>:include</tt> (or any other valid <tt>find</tt> options) to augment the results of your cached query.
40
+
41
+ === Efficiency
42
+
43
+ ARID Cache intercepts calls to <tt>cached_</tt> methods using <tt>method_missing</tt> then defines those methods on your models as they are called, so they bypass method missing on subsequent calls.
44
+
45
+ == Examples
46
+
47
+ ==== Given the following model:
8
48
 
9
49
  class User < ActiveRecord::Base
10
50
  include AridCache
@@ -13,12 +53,11 @@ Given the following model:
13
53
  named_scope :active, :conditions => [ 'updated_at <= ', 5.minutes.ago ]
14
54
  end
15
55
 
16
- ARID Cache provides these methods:
56
+ ==== ARID Cache provides these methods:
17
57
 
18
58
  User.cached_active # caches the user IDs and the count
19
59
  User.cached_active_count # gets the count for free
20
60
 
21
- user = User.first
22
61
  user.cached_pets # caches the pets IDs and the count
23
62
  user.cached_pets_count # gets the count for free
24
63
 
@@ -28,9 +67,9 @@ selects where the IDs are the cached IDs.
28
67
 
29
68
  It also gives you paging using WillPaginate. The IDs from the cache are paginated and
30
69
  only that page is selected from the database - again directly from the table, without
31
- any complex joins or anything.
70
+ any complex joins.
32
71
 
33
- Some examples of pagination:
72
+ ==== Some examples of pagination:
34
73
 
35
74
  User.cached_active.paginate(:page => 1, :per_page => 30)
36
75
  User.cached_active.paginate(:page => 1)
@@ -40,21 +79,32 @@ You can also include options for find, such as <tt>:join</tt>, <tt>:include</tt>
40
79
 
41
80
  User.cached_active.paginate(:page => 1, :include => :preferences)
42
81
  User.cached_active.paginate(:page => 1, :order => 'created_at DESC') # don't change the order, just enforce it
82
+
83
+ You can limit the results returned using <tt>:limit</tt> and <tt>:offset</tt>:
84
+
85
+ user.cached_pets(:limit => 2, :include => :toys)
86
+ user.cached_pets(:limit => 2, :offset => 3, :include => :toys)
43
87
 
44
- You can configure your cached blocks in your models:
45
-
46
- class User < ActiveRecord::Base
47
- include AridCache
48
- has_many :pets
49
- has_one :preferences
50
- named_scope :active, :conditions => [ 'updated_at <= ', 5.minutes.ago ]
51
-
52
- self.cached_active_users(:order => 'created_at DESC', :include => :preferences) do
53
- self.active
54
- end
88
+ ==== You can dynamically create caches
89
+
90
+ User.cached_most_active_users do
91
+ self.active.find(:order => 'activity DESC', :limit => 10)
55
92
  end
56
-
57
93
 
94
+ Dynamic caches that make use of other cached collections:
95
+
96
+ @tracks = @genre.cached_highlight_tracks(:order => 'release_date DESC', :include => [:album, :artist]) do
97
+ cached_tracks(:order => 'release_date DESC', :limit => 10, :include => [:album, :artist])
98
+ end
99
+ @artists = @genre.cached_highlight_artists do
100
+ cached_artists(:limit => 10)
101
+ end
102
+ @albums = @genre.cached_highlight_albums(:order => 'release_date DESC', :include => :artist) do
103
+ cached_albums(:order => 'release_date DESC', :limit => 3, :include => :artist)
104
+ end
105
+
106
+ More docs to come...
107
+
58
108
  == Note on Patches/Pull Requests
59
109
 
60
110
  * Fork the project.
data/Rakefile CHANGED
@@ -6,7 +6,11 @@ begin
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "arid_cache"
8
8
  gem.summary = %Q{Automates efficient caching of your ActiveRecord collections, gives you counts for free and supports pagination.}
9
- gem.description = %Q{}
9
+ gem.description = <<-END.gsub(/^\s+/, '')
10
+ ARID Cache makes caching easy and effective. ARID cache supports caching on all your model named scopes, class methods and instance methods right out of the box. ARID cache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
11
+
12
+ ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
13
+ END
10
14
  gem.email = "kjvarga@gmail.com"
11
15
  gem.homepage = "http://github.com/kjvarga/arid_cache"
12
16
  gem.authors = ["Karl Varga"]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
data/arid_cache.gemspec CHANGED
@@ -5,12 +5,14 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{arid_cache}
8
- s.version = "0.1.0"
8
+ s.version = "0.1.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Karl Varga"]
12
- s.date = %q{2009-12-21}
13
- s.description = %q{}
12
+ s.date = %q{2009-12-25}
13
+ s.description = %q{ARID Cache makes caching easy and effective. ARID cache supports caching on all your model named scopes, class methods and instance methods right out of the box. ARID cache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
14
+ ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
15
+ }
14
16
  s.email = %q{kjvarga@gmail.com}
15
17
  s.extra_rdoc_files = [
16
18
  "LICENSE",
@@ -23,13 +25,13 @@ Gem::Specification.new do |s|
23
25
  "Rakefile",
24
26
  "VERSION",
25
27
  "arid_cache.gemspec",
28
+ "init.rb",
26
29
  "lib/arid_cache.rb",
27
30
  "lib/arid_cache/active_record.rb",
28
31
  "lib/arid_cache/cache_proxy.rb",
32
+ "lib/arid_cache/helpers.rb",
29
33
  "lib/arid_cache/store.rb",
30
34
  "rails/init.rb",
31
- "rails/install.rb",
32
- "rails/uninstall.rb",
33
35
  "spec/arid_cache_spec.rb",
34
36
  "spec/spec.opts",
35
37
  "spec/spec_helper.rb",
data/init.rb ADDED
@@ -0,0 +1,6 @@
1
+ begin
2
+ require File.join(File.dirname(__FILE__), 'lib', 'arid_cache') # From here
3
+ rescue LoadError
4
+ require 'arid_cache' # From gem
5
+ end
6
+ AridCache.init_rails
@@ -1,44 +1,67 @@
1
1
  module AridCache
2
- module ActiveRecord
2
+ module ActiveRecord
3
+ def self.included(base)
4
+ base.extend MirrorMethods
5
+ base.send :include, MirrorMethods
6
+ base.class_eval do
7
+ alias_method_chain :method_missing, :arid_cache
8
+ end
9
+ class << base
10
+ alias_method_chain :method_missing, :arid_cache
11
+ end
12
+ end
13
+
3
14
  module MirrorMethods
15
+ def clear_all_caches
16
+ AridCache.cache.clear_class_caches(self)
17
+ AridCache.cache.clear_instance_caches(self)
18
+ end
19
+
20
+ def clear_class_caches
21
+ AridCache.cache.clear_class_caches(self)
22
+ end
23
+
24
+ def clear_instance_caches
25
+ AridCache.cache.clear_instance_caches(self)
26
+ end
27
+
28
+ def get_singleton
29
+ class << self; self; end
30
+ end
4
31
 
5
32
  # Return a cache key for the given key e.g.
6
33
  # User.arid_cache_key('companies') => 'user-companies'
7
- # User.first.arid_cache_key('companies') => 'users/1-companies'
8
- def arid_cache_key(key, suffix=nil)
9
- arid_cache_key = (self.is_a?(Class) ? self.name.downcase : self.cache_key) + '-' + key.to_s
10
- suffix.nil? ? arid_cache_key : arid_cache_key + suffix
11
- ('arid-cache-' + arid_cache_key).to_sym
34
+ # User.first.arid_cache_key('companies') => 'users/20090120091123-companies'
35
+ def arid_cache_key(key)
36
+ key = (self.is_a?(Class) ? self.name.downcase : self.cache_key) + '-' + key.to_s
37
+ 'arid-cache-' + key
12
38
  end
13
39
 
14
- def clear_cache
15
- AridCache.cache.clear(self)
16
- end
17
-
18
- # Return the cache store from the class
19
- def cache_store
20
- (self.is_a?(Class) ? self : self.class).send(:class_variable_get, :@@cache_store)
21
- end
40
+ def respond_to?(method, include_private = false) #:nodoc:
41
+ if (method.to_s =~ /^class_cache_.*|cache_.*|cached_.*(_count)?$/).nil?
42
+ super(method, include_private)
43
+ elsif method.to_s =~ /^cached_(.*)_count$/
44
+ AridCache.store.has?(self, "#{$1}_count") || AridCache.store.has?(self, $1) || super("#{$1}_count", include_private) || super($1, include_private)
45
+ elsif method.to_s =~ /^cached_(.*)$/
46
+ AridCache.store.has?(self, $1) || super($1, include_private)
47
+ else
48
+ super(method, include_private)
49
+ end
50
+ end
51
+
52
+ protected
22
53
 
23
54
  # Intercept methods beginning with <tt>cached_</tt>
24
- def method_missing_with_arid_cache(method, *args, &block)
25
- if method.to_s =~ /^cached_(.*)$/
26
- args = args.empty? ? {} : args.first
27
- cache_store.query($1, args, self, &block)
55
+ def method_missing_with_arid_cache(method, *args, &block) #:nodoc:
56
+ opts = args.empty? ? {} : args.first
57
+ if method.to_s =~ /^cache_(.*)$/
58
+ AridCache.define(self, $1, opts, &block)
59
+ elsif method.to_s =~ /^cached_(.*)$/
60
+ AridCache.lookup(self, $1, opts, &block)
28
61
  else
29
62
  method_missing_without_arid_cache(method, *args)
30
63
  end
31
- end
32
- alias_method :method_missing_without_arid_cache, :method_missing
33
- alias_method :method_missing, :method_missing_with_arid_cache
34
- end
35
-
36
- def self.included(base)
37
- base.extend MirrorMethods
38
- base.send :include, MirrorMethods
39
- base.class_eval do
40
- @@cache_store = AridCache::Store.new
41
64
  end
42
65
  end
43
66
  end
44
- end
67
+ end
@@ -1,96 +1,164 @@
1
- # AridCache::Cache is a singleton instance
2
1
  module AridCache
3
2
  class CacheProxy
4
- Struct.new('Result', :opts, :ids, :klass, :count) do
3
+ attr_accessor :object, :key, :opts, :blueprint, :cached, :cache_key, :block, :records
4
+
5
+ # AridCache::CacheProxy::Result
6
+ #
7
+ # This struct is stored in the cache and stores information we need
8
+ # to re-query for results.
9
+ Result = Struct.new(:ids, :klass, :count) do
5
10
 
6
11
  def has_count?
7
12
  !count.nil?
8
13
  end
9
-
14
+
10
15
  def has_ids?
11
16
  !ids.nil?
12
17
  end
18
+
19
+ def klass=(value)
20
+ self['klass'] = value.is_a?(Class) ? value.name : value
21
+ end
22
+
23
+ def klass
24
+ self['klass'].constantize unless self['klass'].nil?
25
+ end
13
26
  end
27
+
28
+ def self.clear_all_caches
29
+ Rails.cache.delete_matched(%r[arid-cache-.*])
30
+ end
14
31
 
15
- # Clear the cache of all arid cache entries.
16
- #
17
- # If *object* is passed, only clear cached entries for that object's
18
- # class and instances e.g.
19
- # User.clear_cache deletes 'arid-cache-users/1-companies' as well
20
- # as 'arid-cache-user-companies'
21
- def clear(object=nil)
22
- key = 'arid-cache-'
23
- key += (object.is_a?(Class) ? object : object.class).name.downcase unless object.nil?
24
- Rails.cache.delete_matched(%r[#{key}.*])
25
- end
26
-
27
- def self.instance
28
- @@singleton_instance ||= self.new
29
- end
32
+ def self.clear_class_caches(object)
33
+ key = (object.is_a?(Class) ? object : object.class).name.downcase + '-'
34
+ Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
35
+ end
30
36
 
31
- def has?(object, key)
37
+ def self.clear_instance_caches(object)
38
+ key = (object.is_a?(Class) ? object : object.class).name.pluralize.downcase
39
+ Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
40
+ end
41
+
42
+ def self.has?(object, key)
32
43
  Rails.cache.exist?(object.arid_cache_key(key))
33
44
  end
34
-
35
- def fetch_count(blueprint)
36
- cached = Rails.cache.read(blueprint.cache_key)
37
- if cached.nil?
38
- execute_count(blueprint)
39
- elsif cached.is_a?(Struct::Result)
40
- cached.has_count? ? cached.count : execute_count(blueprint)
45
+
46
+ def self.fetch_count(object, key, opts, &block)
47
+ CacheProxy.new(object, key, opts, &block).fetch_count
48
+ end
49
+
50
+ def self.fetch(object, key, opts, &block)
51
+ CacheProxy.new(object, key, opts, &block).fetch
52
+ end
53
+
54
+ def initialize(object, key, opts, &block)
55
+ self.object = object
56
+ self.key = key
57
+ self.opts = opts || {}
58
+ self.blueprint = AridCache.store.find(object, key)
59
+ self.cache_key = object.arid_cache_key(key)
60
+ self.cached = Rails.cache.read(cache_key)
61
+ self.block = block
62
+ self.records = nil
63
+ end
64
+
65
+ def fetch_count
66
+ if cached.nil? || opts[:force]
67
+ execute_count
68
+ elsif cached.is_a?(AridCache::CacheProxy::Result)
69
+ cached.has_count? ? cached.count : execute_count
70
+ elsif cached.is_a?(Fixnum)
71
+ cached
72
+ elsif cached.respond_to?(:count)
73
+ cached.count
41
74
  else
42
- cached # some base type, return it
75
+ cached # what else can we do? return it
43
76
  end
44
77
  end
45
-
46
- def fetch(blueprint, opts)
47
- cached = Rails.cache.read(blueprint.cache_key)
48
- if cached.nil?
49
- execute_find(blueprint, opts)
50
- elsif cached.is_a?(Struct::Result)
78
+
79
+ def fetch
80
+ if cached.nil? || opts[:force]
81
+ execute_find
82
+ elsif cached.is_a?(AridCache::CacheProxy::Result)
51
83
  if cached.has_ids? # paginate and fetch here
84
+ klass = find_class_of_results
52
85
  if opts.include?(:page)
53
- blueprint.klass.paginate(cached.ids, opts_for_paginate(opts, cached))
86
+ klass.paginate(cached.ids, opts_for_paginate)
54
87
  else
55
- blueprint.klass.find(cached.ids, opts_for_find(opts, cached))
88
+ klass.find(cached.ids, opts_for_find)
56
89
  end
57
90
  else
58
- execute_find(blueprint, opts)
91
+ execute_find
59
92
  end
60
93
  else
61
94
  cached # some base type, return it
62
95
  end
63
96
  end
64
-
97
+
65
98
  private
66
99
 
67
- def execute_find(blueprint, opts)
68
- records = blueprint.proc.call
69
-
70
- if !records.is_a?(Enumerable)
71
- return records # some base type, return it
72
- end
73
-
74
- # Update Rails cache and return the records
75
- cached = Struct::Result.new(blueprint.opts)
76
- cached.ids = records.collect(&:id)
77
- cached.count = records.size
78
- if records.respond_to?(:proxy_reflection) # association proxy
79
- cached.klass = records.proxy_reflection.klass
80
- elsif records.is_a?(Enumerable)
81
- cached.klass = records.empty? ? blueprint.klass : records.first.class
82
- RAILS_DEFAULT_LOGGER.info("** AridCache: inferring class of collection for cache #{blueprint.cache_key} to be #{cached.klass}")
100
+ def get_records
101
+ block = block || blueprint.proc
102
+ self.records = block.nil? ? object.instance_eval(key) : object.instance_eval(&block)
103
+ end
104
+
105
+ def execute_find
106
+ get_records
107
+ cached = AridCache::CacheProxy::Result.new
108
+
109
+ if !records.is_a?(Enumerable) || (!records.empty? && !records.first.is_a?(::ActiveRecord::Base))
110
+ cached = records # some base type, cache it as itself
111
+ else
112
+ cached.ids = records.collect(&:id)
113
+ cached.count = records.size
114
+ if records.respond_to?(:proxy_reflection) # association proxy
115
+ cached.klass = records.proxy_reflection.klass
116
+ elsif !records.empty?
117
+ cached.klass = records.first.class
118
+ else
119
+ cached.klass = object_base_class
120
+ end
83
121
  end
122
+ Rails.cache.write(cache_key, cached)
84
123
 
85
- Rails.cache.write(blueprint.cache_key, cached)
86
- opts.include?(:page) ? records.paginate(opts_for_paginate(opts, cached)) : records
124
+ self.cached = cached
125
+ return_records(records)
126
+ end
127
+
128
+ # Convert records to an array before calling paginate. If we don't do this
129
+ # and the result is a named scope, paginate will trigger an additional query
130
+ # to load the page rather than just using the records we have already fetched.
131
+ #
132
+ # If we are not paginating and the options include :limit (and optionally :offset)
133
+ # apply the limit and offset to the records before returning them.
134
+ #
135
+ # Otherwise we have an issue where all the records are returned the first time
136
+ # the collection is loaded, but on subsequent calls the options_for_find are
137
+ # included and you get different results. Note that with options like :order
138
+ # this cannot be helped. We don't want to modify the query that generates the
139
+ # collection because the idea is to allow getting different perspectives of the
140
+ # cached collection without relying on modifying the collection as a whole.
141
+ #
142
+ # If you do want a specialized, modified, or subset of the collection it's best
143
+ # to define it in a block and have a new cache for it:
144
+ #
145
+ # model.my_special_collection { the_collection(:order => 'new order') }
146
+ def return_records(records)
147
+ if opts.include?(:page)
148
+ records = records.respond_to?(:to_a) ? records.to_a : records
149
+ records.respond_to?(:paginate) ? records.paginate(opts_for_paginate) : records
150
+ elsif opts.include?(:limit)
151
+ records = records.respond_to?(:to_a) ? records.to_a : records
152
+ offset = opts[:offset] || 0
153
+ records[offset, opts[:limit]]
154
+ else
155
+ records
156
+ end
87
157
  end
88
158
 
89
- def execute_count(blueprint)
90
- records = blueprint.proc.call
91
-
92
- # Update Rails cache and return the count
93
- cached = Struct::Result.new(blueprint.opts)
159
+ def execute_count
160
+ get_records
161
+ cached = AridCache::CacheProxy::Result.new
94
162
 
95
163
  # Just get the count if we can.
96
164
  #
@@ -103,42 +171,39 @@ module AridCache
103
171
  # and passes the respond_to? on to it.
104
172
  if records.respond_to?(:proxy_reflection) || records.respond_to?(:proxy_options)
105
173
  cached.count = records.count # just get the count
106
- cached.klass = blueprint.klass
107
- elsif records.is_a?(Enumerable)
174
+ cached.klass = object_base_class
175
+ elsif records.is_a?(Enumerable) && (records.empty? || records.first.is_a?(::ActiveRecord::Base))
108
176
  cached.ids = records.collect(&:id) # get everything now that we have it
109
177
  cached.count = records.size
110
- cached.klass = records.empty? ? blueprint.klass : records.first.class
111
- Rails.logger.info("** AridCache: inferring class of collection for cache #{blueprint.cache_key} to be #{cached.klass}")
178
+ cached.klass = records.empty? ? object_base_class : records.first.class
112
179
  else
113
180
  cached = records # some base type, cache it as itself
114
181
  end
115
182
 
116
- Rails.cache.write(blueprint.cache_key, cached)
117
- cached.count
118
- end
119
-
120
- def paginate(records, opts, proc=nil)
121
- if !proc.nil?
122
- ids = opts.include?(:page) ? records.paginate(opts) : records
123
- records = proc.call(ids)
124
- ids.is_a?(WillPaginate::Collection) ? ids.replace(records) : records
125
- else
126
- opts.include?(:page) ? records.paginate(opts) : records
127
- end
183
+ Rails.cache.write(cache_key, cached)
184
+ self.cached = cached
185
+ cached.respond_to?(:count) ? cached.count : cached
128
186
  end
129
187
 
130
- def opts_for_paginate(opts, cached)
131
- opts = cached.opts.merge(opts.symbolize_keys)
132
- opts[:total_entries] = cached.count
133
- opts
188
+ def opts_for_paginate
189
+ paginate_opts = blueprint.nil? ? opts.symbolize_keys : blueprint.opts.merge(opts.symbolize_keys)
190
+ paginate_opts[:total_entries] = cached.count
191
+ paginate_opts
134
192
  end
135
193
 
136
- def opts_for_find(opts, cached)
137
- opts = cached.opts.merge(opts.symbolize_keys)
138
- opts.values_at([:include, :joins, :conditions, :order, :group, :having]).compact
194
+ VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock ]
195
+
196
+ def opts_for_find
197
+ find_opts = blueprint.nil? ? opts.symbolize_keys : blueprint.opts.merge(opts.symbolize_keys)
198
+ find_opts.delete_if { |k,v| !VALID_FIND_OPTIONS.include?(k) }
139
199
  end
140
-
141
- def initialize
200
+
201
+ def object_base_class
202
+ object.is_a?(Class) ? object : object.class
203
+ end
204
+
205
+ def find_class_of_results
206
+ opts[:class] || (blueprint && blueprint.opts[:class]) || cached.klass || object_base_class
142
207
  end
143
208
  end
144
209
  end
@@ -0,0 +1,86 @@
1
+ module AridCache
2
+ module Helpers
3
+
4
+ # Lookup something from the cache.
5
+ #
6
+ # If no block is provided, create one dynamically. If a block is
7
+ # provided, it is only used the first time it is encountered.
8
+ # This allows you to dynamically define your caches while still
9
+ # returning the results of your query.
10
+ #
11
+ # @return a WillPaginate::Collection if the options include :page,
12
+ # a Fixnum count if the request is for a count or the results of
13
+ # the ActiveRecord query otherwise.
14
+ def lookup(object, key, opts, &block)
15
+ if !block.nil?
16
+ define(object, key, opts, &block)
17
+ elsif key =~ /(.*)_count$/
18
+ if AridCache.store.has?(object, $1)
19
+ method_for_cached(object, $1, :fetch_count, key)
20
+ elsif object.respond_to?(key)
21
+ define(object, key, opts, :fetch_count)
22
+ elsif object.respond_to?($1)
23
+ define(object, $1, opts, :fetch_count, key)
24
+ else
25
+ raise ArgumentError.new("#{object} doesn't respond to #{key} or #{$1}! Cannot dynamically create query to get the count, please call with a block.")
26
+ end
27
+ elsif object.respond_to?(key)
28
+ define(object, key, opts, &block)
29
+ else
30
+ raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create query, please call with a block.")
31
+ end
32
+ object.send("cached_#{key}", opts)
33
+ end
34
+
35
+ # Store the options and optional block for a call to the cache.
36
+ #
37
+ # If no block is provided, create one dynamically.
38
+ #
39
+ # @return an AridCache::Store::Blueprint.
40
+ def define(object, key, opts, fetch_method=:fetch, method_name=nil, &block)
41
+ if block.nil? && !object.respond_to?(key)
42
+ raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create a block for your cache item.")
43
+ end
44
+
45
+ # FIXME: Pass default options to store.add
46
+ # Pass nil in for now until we get the cache_ calls working.
47
+ # This means that the first time you define a dynamic cache
48
+ # (by passing in a block), the options you used are not
49
+ # stored in the blueprint and applied to each subsequent call.
50
+ #
51
+ # Otherwise we have a situation where a :limit passed in to the
52
+ # first call persists when no options are passed in on subsequent calls,
53
+ # but if a different :limit is passed in that limit is applied.
54
+ #
55
+ # I think in this scenario one would expect no limit to be applied
56
+ # if no options are passed in.
57
+ #
58
+ # When the cache_ methods are supported, those options should be
59
+ # remembered and applied to the collection however.
60
+ blueprint = AridCache.store.add(object, key, block, nil)
61
+ method_for_cached(object, key, fetch_method, method_name)
62
+ blueprint
63
+ end
64
+
65
+ private
66
+
67
+ def method_for_cached(object, key, fetch_method=:fetch, method_name=nil)
68
+ method_name = "cached_" + (method_name || key)
69
+ if object.is_a?(Class)
70
+ (class << object; self; end).instance_eval do
71
+ define_method(method_name) do |*args, &block|
72
+ opts = args.empty? ? {} : args.first
73
+ AridCache.cache.send(fetch_method, self, key, opts, &block)
74
+ end
75
+ end
76
+ else
77
+ object.class_eval do
78
+ define_method(method_name) do |*args, &block|
79
+ opts = args.empty? ? {} : args.first
80
+ AridCache.cache.send(fetch_method, self, key, opts, &block)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,58 +1,84 @@
1
1
  module AridCache
2
2
  class Store < Hash
3
- Struct.new('Item', :cache_key, :proc, :klass, :opts)
3
+ extend ActiveSupport::Memoizable
4
4
 
5
- def query(key, opts, object, &block)
6
- return store(object, key, Proc.new, opts) if block_given? # store a proc
5
+ # AridCache::Store::Blueprint
6
+ #
7
+ # Stores options and blocks that are used to generate results for finds
8
+ # and counts.
9
+ Blueprint = Struct.new(:key, :klass, :proc, :opts) do
7
10
 
8
- if has?(object, key)
9
- AridCache.cache.fetch(find(object, key), opts)
10
- elsif key =~ /(.*)_count$/
11
- if has?(object, $1)
12
- AridCache.cache.fetch_count(find(object, $1))
13
- elsif object.respond_to?(key)
14
- AridCache.cache.fetch_count(find_or_create(object, key))
15
- elsif object.respond_to?($1)
16
- AridCache.cache.fetch_count(find_or_create(object, $1))
17
- else
18
- raise ArgumentError.new("#{object} doesn't respond to #{key} or #{$1}! Cannot dynamically create query to get the count.")
19
- end
20
- else
21
- if object.respond_to?(key)
22
- AridCache.cache.fetch(find_or_create(object, key), opts)
11
+ def initialize(key, klass, proc=nil, opts={})
12
+ self.key = key
13
+ self.klass = klass
14
+ self.proc = proc
15
+ self.opts = opts
16
+ end
17
+
18
+ def klass=(value) # store the base class of *value*
19
+ self['klass'] = value.is_a?(Class) ? value.name : value.class.name
20
+ end
21
+
22
+ def klass
23
+ self['klass'].constantize unless self['klass'].nil?
24
+ end
25
+
26
+ def opts=(value)
27
+ self['opts'] = value.symbolize_keys! unless !value.respond_to?(:symbolize_keys)
28
+ end
29
+
30
+ def opts
31
+ self['opts'] || {}
32
+ end
33
+
34
+ def proc(object=nil)
35
+ if self['proc'].nil? && !object.nil?
36
+ self['proc'] = key
23
37
  else
24
- raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create query.")
38
+ self['proc']
25
39
  end
26
- end
40
+ end
27
41
  end
28
42
 
29
- # Store a proc
30
- def store(object, key, proc, opts)
31
- cache_key = object.arid_cache_key(key)
32
- self[cache_key] = Struct::Item.new(cache_key, proc, (object.is_a?(Class) ? object : object.class), opts.symbolize_keys!)
43
+ def has?(object, key)
44
+ self.include?(object_store_key(object, key))
33
45
  end
34
46
 
47
+ # Empty the proc store
48
+ def delete!
49
+ delete_if { true }
50
+ end
51
+
52
+ def self.instance
53
+ @@singleton_instance ||= self.new
54
+ end
55
+
35
56
  def find(object, key)
36
- self[object.arid_cache_key(key)]
57
+ self[object_store_key(object, key)]
58
+ end
59
+
60
+ def add(object, key, proc, opts)
61
+ store_key = object_store_key(object, key)
62
+ self[store_key] = AridCache::Store::Blueprint.new(key, object, proc, opts)
37
63
  end
38
64
 
39
- # Find or dynamically create a proc
40
65
  def find_or_create(object, key)
41
- cache_key = object.arid_cache_key(key)
42
- if include?(cache_key)
43
- self[cache_key]
66
+ store_key = object_store_key(object, key)
67
+ if self.include?(store_key)
68
+ self[store_key]
44
69
  else
45
- self[cache_key] = Struct::Item.new(cache_key, Proc.new { object.send(key) }, (object.is_a?(Class) ? object : object.class))
70
+ self[store_key] = AridCache::Store::Blueprint.new(key, object)
46
71
  end
47
72
  end
48
73
 
49
- def has?(object, key)
50
- self.include?(object.arid_cache_key(key))
74
+ protected
75
+
76
+ def initialize
51
77
  end
52
78
 
53
- # Empty the proc store
54
- def delete!
55
- delete_if { true }
56
- end
79
+ def object_store_key(object, key)
80
+ (object.is_a?(Class) ? object.name.downcase : object.class.name.pluralize.downcase) + '-' + key.to_s
81
+ end
82
+ memoize :object_store_key
57
83
  end
58
84
  end
data/lib/arid_cache.rb CHANGED
@@ -1,15 +1,47 @@
1
+ dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
3
+
4
+ require 'arid_cache/helpers'
1
5
  require 'arid_cache/store'
2
6
  require 'arid_cache/active_record'
3
7
  require 'arid_cache/cache_proxy'
4
8
 
5
9
  module AridCache
10
+ extend AridCache::Helpers
6
11
  class Error < StandardError; end
7
12
 
8
13
  def self.cache
9
- AridCache::CacheProxy.instance
14
+ AridCache::CacheProxy
10
15
  end
11
16
 
17
+ def self.clear_all_caches
18
+ AridCache::CacheProxy.clear_all_caches
19
+ end
20
+
21
+ def self.clear_class_caches(object)
22
+ AridCache::CacheProxy.clear_class_caches(object)
23
+ end
24
+
25
+ def self.clear_instance_caches(object)
26
+ AridCache::CacheProxy.clear_instance_caches(object)
27
+ end
28
+
29
+ def self.store
30
+ AridCache::Store.instance
31
+ end
32
+
33
+ # The old method of including this module, if you don't want to
34
+ # extend active record. Just add 'include AridCache' to your
35
+ # model class.
12
36
  def self.included(base)
13
37
  base.send(:include, AridCache::ActiveRecord)
14
38
  end
39
+
40
+ # Initializes ARID Cache for Rails.
41
+ #
42
+ # This method is called by `init.rb`,
43
+ # which is run by Rails on startup.
44
+ def self.init_rails
45
+ ::ActiveRecord::Base.send(:include, AridCache::ActiveRecord)
46
+ end
15
47
  end
data/rails/init.rb CHANGED
@@ -1 +1 @@
1
- require 'arid_cache'
1
+ Kernel.load File.join(File.dirname(__FILE__), '..', 'init.rb')
@@ -3,37 +3,44 @@ require File.join(File.dirname(__FILE__), 'test_helper')
3
3
  class AridCacheTest < ActiveSupport::TestCase
4
4
  def setup
5
5
  Rails.cache.clear
6
+ AridCache.store.delete!
6
7
  get_user
7
8
  end
8
-
9
+
10
+ test "initializes needed objects" do
11
+ assert_instance_of AridCache::Store, AridCache.store
12
+ assert_same AridCache::CacheProxy, AridCache.cache
13
+ end
14
+
9
15
  test "should respond to methods" do
10
- assert_respond_to(User, :cache_store)
11
- assert_respond_to(User.first, :cache_store)
12
- assert_instance_of AridCache::Store, User.cache_store
13
- assert_same User.cache_store, User.first.cache_store
16
+ assert User.respond_to?(:clear_cache)
17
+ assert User.first.respond_to?(:clear_cache)
18
+ assert_instance_of AridCache::Store, AridCache.store
14
19
  end
15
20
 
16
- test "should not clobber method_missing" do
17
- assert_respond_to User.first, :name
18
- end
19
-
20
- test "should allow access to valid methods" do
21
+ test "should not clobber model methods" do
22
+ assert_respond_to User.first, :name
23
+ assert_respond_to Company.first, :name
24
+ assert_nothing_raised { User.first.name }
25
+ assert_nothing_raised { Company.first.name }
26
+
27
+ # Shouldn't mess with your model's method_missing
21
28
  assert_nothing_raised { User.first.is_high? }
22
- assert User.first.is_high?
23
- end
29
+ assert User.first.is_high?
30
+ end
24
31
 
25
32
  test "should allow me to cache on the model" do
26
33
  assert_nothing_raised do
27
34
  define_model_cache(User)
28
35
  end
29
- assert_instance_of(Proc, User.cache_store[User.arid_cache_key('companies')].proc)
36
+ #assert_instance_of(Proc, AridCache.store[User.arid_cache_key('companies')].proc)
30
37
  end
31
38
 
32
39
  test "should allow me to cache on the instance" do
33
40
  assert_nothing_raised do
34
41
  define_instance_cache(@user)
35
42
  end
36
- assert_instance_of(Proc, @user.cache_store[@user.arid_cache_key('companies')].proc)
43
+ #assert_instance_of(Proc, AridCache.store[AridCache.store.store_key@user.arid_cache_key('companies')].proc)
37
44
  end
38
45
 
39
46
  test "should raise an error on invalid dynamic caches" do
@@ -44,7 +51,7 @@ class AridCacheTest < ActiveSupport::TestCase
44
51
 
45
52
  test "should create dynamic caches given valid arguments" do
46
53
  assert_nothing_raised { @user.cached_companies }
47
- assert_instance_of(Proc, @user.cache_store[@user.arid_cache_key('companies')].proc)
54
+ #assert_instance_of(Proc, AridCache.store[@user.arid_cache_key('companies')].proc)
48
55
  end
49
56
 
50
57
  test "counts queries correctly" do
@@ -58,9 +65,9 @@ class AridCacheTest < ActiveSupport::TestCase
58
65
  end
59
66
 
60
67
  test "paginates results" do
61
- results = @user.cached_companies(:page => 1)
68
+ results = @user.cached_companies(:page => 1, :per_page => 3)
62
69
  assert_kind_of WillPaginate::Collection, results
63
- assert_equal 2, results.size
70
+ assert_equal 3, results.size
64
71
  assert_equal @user.companies.count, results.total_entries
65
72
  assert_equal 1, results.current_page
66
73
  end
@@ -75,15 +82,16 @@ class AridCacheTest < ActiveSupport::TestCase
75
82
  test "works for different pages" do
76
83
  results = @user.cached_companies(:page => 2, :per_page => 3)
77
84
  assert_kind_of WillPaginate::Collection, results
78
- assert_equal (@user.companies.count-3)%3, results.size
79
- assert_equal @user.companies.count, results.total_entries
85
+ assert results.size <= 3
86
+ assert_equal @user.companies.count, results.total_entries
87
+ assert_equal 2, results.current_page
80
88
  end
81
-
89
+
82
90
  test "ignores random parameters" do
83
91
  result = @user.cached_companies(:invalid => :params, 'random' => 'values', :user_id => 3)
84
92
  assert_equal @user.companies, result
85
93
  end
86
-
94
+
87
95
  test "passes on options to find" do
88
96
  actual = @user.cached_companies(:order => 'users.id DESC')
89
97
  expected = @user.companies
@@ -99,31 +107,101 @@ class AridCacheTest < ActiveSupport::TestCase
99
107
  end
100
108
 
101
109
  test "gets the count only if it's requested first" do
110
+ count = @user.companies.count
111
+ assert_queries(1) do
112
+ assert_equal count, @user.cached_companies_count
113
+ assert_equal count, @user.cached_companies_count
114
+ end
115
+ assert_queries(1) do
116
+ assert_equal count, @user.cached_companies.size
117
+ assert_equal count, @user.cached_companies_count
118
+ end
119
+ end
120
+
121
+ test "calling cache_ defines methods on the object" do
122
+ assert !User.method_defined?(:cached_favorite_companies)
123
+ User.cache_favorite_companies(:order => 'name DESC') do
124
+ User.companies
125
+ end
126
+ assert User.respond_to?(:cached_favorite_companies)
127
+ assert_nothing_raised do
128
+ User.method(:cached_favorite_companies)
129
+ end
130
+ end
131
+
132
+ test "applies limit and offset" do
133
+ @user.cached_limit_companies do
134
+ companies
135
+ end
136
+ assert_equal 2, @user.cached_limit_companies(:limit => 2).size
137
+ assert_equal 3, @user.cached_limit_companies(:limit => 3).size
138
+ assert_equal @user.companies.all(:limit => 2, :offset => 1), @user.cached_limit_companies(:limit => 2, :offset => 1)
139
+ assert_equal @user.companies.size, @user.cached_limit_companies.size
140
+ User.cached_successful_limit_companies do
141
+ User.successful
142
+ end
143
+ raise User.cached_successful_limit_companies.inspect
144
+ assert_equal 2, User.cached_successful_limit_companies(:limit => 2).size
145
+ assert_equal 3, User.cached_successful_limit_companies(:limit => 3).size
146
+ assert_equal User.successful.all(:limit => 2, :offset => 1), User.cached_successful_limit_companies(:limit => 2, :offset => 1)
147
+ assert_equal User.successful.size, User.cached_successful_limit_companies.size
148
+ end
149
+
150
+ test "pagination should not result in an extra query" do
151
+ assert_queries(1) do
152
+ @user.cached_big_companies(:page => 1)
153
+ end
102
154
  assert_queries(1) do
103
- assert_equal 5, @user.cached_companies_count
104
- assert_equal 5, @user.cached_companies_count
155
+ User.cached_companies(:page => 1)
105
156
  end
157
+ end
158
+
159
+ test "should support a 'force' option" do
160
+ # ActiveRecord caches the result of the proc, so we need to
161
+ # use different instances of the user to test the force option.
162
+ uncached_user = User.first
163
+ companies = @user.companies
164
+ size = companies.size
106
165
  assert_queries(1) do
107
- assert_equal 5, @user.cached_companies.size
108
- assert_equal 5, @user.cached_companies_count
109
- end
110
- end
111
-
112
- # test "should empty the Rails cache" do
113
- # @user.cached_companies
114
- # User.cached_companies
115
- # assert Rails.cache.exist?(@user.arid_cache_key('companies'))
116
- # assert Rails.cache.exist?(User.arid_cache_key('companies'))
117
- # User.clear_cache
118
- # assert Rails.cache.exist?(@user.arid_cache_key('companies'))
119
- # assert Rails.cache.exist?(User.arid_cache_key('companies'))
120
- # end
166
+ assert_equal companies, @user.cached_companies
167
+ assert_equal size, @user.cached_companies_count
168
+ assert_equal size, uncached_user.cached_companies_count
169
+ end
170
+ assert_queries(2) do
171
+ assert_equal companies, uncached_user.cached_companies(:force => true)
172
+ assert_equal size, uncached_user.cached_companies_count(:force => true)
173
+ end
174
+ end
175
+
176
+ test "should handle various different model instances" do
177
+ one = User.first
178
+ two = User.first :offset => 1
179
+ assert_not_same one, two
180
+ assert_equal one.companies, one.cached_companies
181
+ assert_equal two.companies, two.cached_companies
182
+ end
183
+
184
+ test "should handle arrays of non-active record instances" do
185
+ assert_equal @user.pet_names, @user.cached_pet_names
186
+ assert_equal @user.pet_names, @user.cached_pet_names
187
+ assert_equal @user.pet_names.count, @user.cached_pet_names_count
188
+ end
189
+
190
+ test "should empty the Rails cache" do
191
+ define_model_cache(User)
192
+ @user.cached_companies
193
+ User.cached_companies
194
+ assert Rails.cache.exist?(@user.arid_cache_key('companies'))
195
+ assert Rails.cache.exist?(User.arid_cache_key('companies'))
196
+ User.clear_cache
197
+ assert Rails.cache.exist?(@user.arid_cache_key('companies'))
198
+ assert Rails.cache.exist?(User.arid_cache_key('companies'))
199
+ end
121
200
 
122
201
  protected
123
202
 
124
203
  def get_user
125
204
  @user = User.first
126
- @user.cache_store.delete!
127
205
  @user.clear_cache
128
206
  define_instance_cache(@user)
129
207
  @user
@@ -141,13 +219,13 @@ class AridCacheTest < ActiveSupport::TestCase
141
219
  end
142
220
 
143
221
  def define_instance_cache(user)
144
- user.cached_companies(:per_page => 2) do
222
+ user.cache_companies(:per_page => 2) do
145
223
  user.companies
146
224
  end
147
225
  end
148
226
 
149
227
  def define_model_cache(model)
150
- model.cached_companies(:per_page => 2) do
228
+ model.cache_companies(:per_page => 2) do
151
229
  model.companies
152
230
  end
153
231
  end
data/test/db/schema.rb CHANGED
@@ -8,5 +8,6 @@ ActiveRecord::Schema.define do
8
8
  t.column "name", :text
9
9
  t.column "owner_id", :integer
10
10
  t.column "country_id", :integer
11
+ t.column "employees", :integer
11
12
  end
12
13
  end
@@ -1,5 +1,6 @@
1
- <% for digit in 1..5 %>
1
+ <% for digit in 1..15 %>
2
2
  acme_<%= digit %>:
3
3
  name: Acme <%= digit %>
4
- owner_id: 1
4
+ owner_id: <%= rand(10) %>
5
+ employees: <%= rand(200) %>
5
6
  <% end %>
@@ -1,3 +1,5 @@
1
- bob:
2
- name: Bob
3
- email: bob@ibm.com
1
+ <% for digit in 1..10 %>
2
+ user_<%= digit %>:
3
+ name: Bob <%= digit %>
4
+ email: bob<%= digit %>@ibm.com
5
+ <% end %>
data/test/models/user.rb CHANGED
@@ -1,9 +1,17 @@
1
1
  require 'arid_cache'
2
2
 
3
3
  class User < ActiveRecord::Base
4
- include AridCache
5
4
  has_many :companies, :foreign_key => :owner_id
6
5
  named_scope :companies, :joins => :companies
6
+ named_scope :successful, :joins => :companies, :conditions => 'companies.employees > 50'
7
+
8
+ def big_companies
9
+ companies.find :all, :conditions => [ 'employees > 20' ]
10
+ end
11
+
12
+ def pet_names
13
+ ['Fuzzy', 'Peachy']
14
+ end
7
15
 
8
16
  def method_missing(method, *args)
9
17
  if method == :is_high?
data/test/test_helper.rb CHANGED
@@ -7,7 +7,11 @@ require 'active_support'
7
7
  require 'active_support/test_case'
8
8
  require 'test/unit' # required by ActiveSupport::TestCase
9
9
  require 'will_paginate'
10
+ require 'ruby-debug'
11
+
12
+ # Activate ARID Cache
10
13
  require 'arid_cache'
14
+ AridCache.init_rails
11
15
 
12
16
  # Setup logging
13
17
  log = File.expand_path(File.join(File.dirname(__FILE__), 'log', 'test.log'))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arid_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karl Varga
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-21 00:00:00 +08:00
12
+ date: 2009-12-25 00:00:00 +08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -42,7 +42,10 @@ dependencies:
42
42
  - !ruby/object:Gem::Version
43
43
  version: "0"
44
44
  version:
45
- description: ""
45
+ description: |
46
+ ARID Cache makes caching easy and effective. ARID cache supports caching on all your model named scopes, class methods and instance methods right out of the box. ARID cache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
47
+ ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
48
+
46
49
  email: kjvarga@gmail.com
47
50
  executables: []
48
51
 
@@ -58,13 +61,13 @@ files:
58
61
  - Rakefile
59
62
  - VERSION
60
63
  - arid_cache.gemspec
64
+ - init.rb
61
65
  - lib/arid_cache.rb
62
66
  - lib/arid_cache/active_record.rb
63
67
  - lib/arid_cache/cache_proxy.rb
68
+ - lib/arid_cache/helpers.rb
64
69
  - lib/arid_cache/store.rb
65
70
  - rails/init.rb
66
- - rails/install.rb
67
- - rails/uninstall.rb
68
71
  - spec/arid_cache_spec.rb
69
72
  - spec/spec.opts
70
73
  - spec/spec_helper.rb
data/rails/install.rb DELETED
@@ -1 +0,0 @@
1
- # Install hook code here
data/rails/uninstall.rb DELETED
@@ -1 +0,0 @@
1
- # Uninstall hook code here