arid_cache 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,50 +1,10 @@
1
1
  == ARID Cache
2
2
 
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.
3
+ ARID Cache is a DRY ActiveRecord ID cache.
4
4
 
5
- ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
5
+ == Example
6
6
 
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:
7
+ Given the following model:
48
8
 
49
9
  class User < ActiveRecord::Base
50
10
  include AridCache
@@ -53,11 +13,12 @@ ARID Cache intercepts calls to <tt>cached_</tt> methods using <tt>method_missing
53
13
  named_scope :active, :conditions => [ 'updated_at <= ', 5.minutes.ago ]
54
14
  end
55
15
 
56
- ==== ARID Cache provides these methods:
16
+ ARID Cache provides these methods:
57
17
 
58
18
  User.cached_active # caches the user IDs and the count
59
19
  User.cached_active_count # gets the count for free
60
20
 
21
+ user = User.first
61
22
  user.cached_pets # caches the pets IDs and the count
62
23
  user.cached_pets_count # gets the count for free
63
24
 
@@ -67,9 +28,9 @@ selects where the IDs are the cached IDs.
67
28
 
68
29
  It also gives you paging using WillPaginate. The IDs from the cache are paginated and
69
30
  only that page is selected from the database - again directly from the table, without
70
- any complex joins.
31
+ any complex joins or anything.
71
32
 
72
- ==== Some examples of pagination:
33
+ Some examples of pagination:
73
34
 
74
35
  User.cached_active.paginate(:page => 1, :per_page => 30)
75
36
  User.cached_active.paginate(:page => 1)
@@ -79,32 +40,21 @@ You can also include options for find, such as <tt>:join</tt>, <tt>:include</tt>
79
40
 
80
41
  User.cached_active.paginate(:page => 1, :include => :preferences)
81
42
  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)
87
43
 
88
- ==== You can dynamically create caches
89
-
90
- User.cached_most_active_users do
91
- self.active.find(:order => 'activity DESC', :limit => 10)
92
- end
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
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 ]
105
51
 
106
- More docs to come...
52
+ self.cached_active_users(:order => 'created_at DESC', :include => :preferences) do
53
+ self.active
54
+ end
55
+ end
107
56
 
57
+
108
58
  == Note on Patches/Pull Requests
109
59
 
110
60
  * Fork the project.
data/Rakefile CHANGED
@@ -6,11 +6,7 @@ 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 = <<-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
9
+ gem.description = %Q{}
14
10
  gem.email = "kjvarga@gmail.com"
15
11
  gem.homepage = "http://github.com/kjvarga/arid_cache"
16
12
  gem.authors = ["Karl Varga"]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.5
1
+ 0.1.0
data/arid_cache.gemspec CHANGED
@@ -5,14 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{arid_cache}
8
- s.version = "0.0.5"
8
+ s.version = "0.1.0"
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-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
- }
12
+ s.date = %q{2009-12-21}
13
+ s.description = %q{}
16
14
  s.email = %q{kjvarga@gmail.com}
17
15
  s.extra_rdoc_files = [
18
16
  "LICENSE",
@@ -25,13 +23,13 @@ ARID Cache is designed for handling large, expensive ActiveRecord collections bu
25
23
  "Rakefile",
26
24
  "VERSION",
27
25
  "arid_cache.gemspec",
28
- "init.rb",
29
26
  "lib/arid_cache.rb",
30
27
  "lib/arid_cache/active_record.rb",
31
28
  "lib/arid_cache/cache_proxy.rb",
32
- "lib/arid_cache/helpers.rb",
33
29
  "lib/arid_cache/store.rb",
34
30
  "rails/init.rb",
31
+ "rails/install.rb",
32
+ "rails/uninstall.rb",
35
33
  "spec/arid_cache_spec.rb",
36
34
  "spec/spec.opts",
37
35
  "spec/spec_helper.rb",
@@ -1,67 +1,44 @@
1
1
  module AridCache
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
-
2
+ module ActiveRecord
14
3
  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
31
4
 
32
5
  # Return a cache key for the given key e.g.
33
6
  # User.arid_cache_key('companies') => 'user-companies'
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
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
38
12
  end
39
13
 
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
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
53
22
 
54
23
  # Intercept methods beginning with <tt>cached_</tt>
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)
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)
61
28
  else
62
29
  method_missing_without_arid_cache(method, *args)
63
30
  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
64
41
  end
65
42
  end
66
43
  end
67
- end
44
+ end
@@ -1,164 +1,96 @@
1
+ # AridCache::Cache is a singleton instance
1
2
  module AridCache
2
3
  class CacheProxy
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
4
+ Struct.new('Result', :opts, :ids, :klass, :count) do
10
5
 
11
6
  def has_count?
12
7
  !count.nil?
13
8
  end
14
-
9
+
15
10
  def has_ids?
16
11
  !ids.nil?
17
12
  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
26
13
  end
27
-
28
- def self.clear_all_caches
29
- Rails.cache.delete_matched(%r[arid-cache-.*])
30
- end
31
-
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
36
-
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
14
 
42
- def self.has?(object, key)
43
- Rails.cache.exist?(object.arid_cache_key(key))
44
- end
45
-
46
- def self.fetch_count(object, key, opts, &block)
47
- CacheProxy.new(object, key, opts, &block).fetch_count
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}.*])
48
25
  end
49
-
50
- def self.fetch(object, key, opts, &block)
51
- CacheProxy.new(object, key, opts, &block).fetch
26
+
27
+ def self.instance
28
+ @@singleton_instance ||= self.new
52
29
  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
30
+
31
+ def has?(object, key)
32
+ Rails.cache.exist?(object.arid_cache_key(key))
63
33
  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
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)
74
41
  else
75
- cached # what else can we do? return it
42
+ cached # some base type, return it
76
43
  end
77
44
  end
78
-
79
- def fetch
80
- if cached.nil? || opts[:force]
81
- execute_find
82
- elsif cached.is_a?(AridCache::CacheProxy::Result)
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)
83
51
  if cached.has_ids? # paginate and fetch here
84
- klass = find_class_of_results
85
52
  if opts.include?(:page)
86
- klass.paginate(cached.ids, opts_for_paginate)
53
+ blueprint.klass.paginate(cached.ids, opts_for_paginate(opts, cached))
87
54
  else
88
- klass.find(cached.ids, opts_for_find)
55
+ blueprint.klass.find(cached.ids, opts_for_find(opts, cached))
89
56
  end
90
57
  else
91
- execute_find
58
+ execute_find(blueprint, opts)
92
59
  end
93
60
  else
94
61
  cached # some base type, return it
95
62
  end
96
63
  end
97
-
64
+
98
65
  private
99
66
 
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
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}")
121
83
  end
122
- Rails.cache.write(cache_key, cached)
123
84
 
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
85
+ Rails.cache.write(blueprint.cache_key, cached)
86
+ opts.include?(:page) ? records.paginate(opts_for_paginate(opts, cached)) : records
157
87
  end
158
88
 
159
- def execute_count
160
- get_records
161
- cached = AridCache::CacheProxy::Result.new
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)
162
94
 
163
95
  # Just get the count if we can.
164
96
  #
@@ -171,39 +103,42 @@ module AridCache
171
103
  # and passes the respond_to? on to it.
172
104
  if records.respond_to?(:proxy_reflection) || records.respond_to?(:proxy_options)
173
105
  cached.count = records.count # just get the count
174
- cached.klass = object_base_class
175
- elsif records.is_a?(Enumerable) && (records.empty? || records.first.is_a?(::ActiveRecord::Base))
106
+ cached.klass = blueprint.klass
107
+ elsif records.is_a?(Enumerable)
176
108
  cached.ids = records.collect(&:id) # get everything now that we have it
177
109
  cached.count = records.size
178
- cached.klass = records.empty? ? object_base_class : records.first.class
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}")
179
112
  else
180
113
  cached = records # some base type, cache it as itself
181
114
  end
182
115
 
183
- Rails.cache.write(cache_key, cached)
184
- self.cached = cached
185
- cached.respond_to?(:count) ? cached.count : cached
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
186
128
  end
187
129
 
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
130
+ def opts_for_paginate(opts, cached)
131
+ opts = cached.opts.merge(opts.symbolize_keys)
132
+ opts[:total_entries] = cached.count
133
+ opts
192
134
  end
193
135
 
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) }
199
- end
200
-
201
- def object_base_class
202
- object.is_a?(Class) ? object : object.class
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
203
139
  end
204
-
205
- def find_class_of_results
206
- opts[:class] || (blueprint && blueprint.opts[:class]) || cached.klass || object_base_class
140
+
141
+ def initialize
207
142
  end
208
143
  end
209
144
  end
@@ -1,84 +1,58 @@
1
1
  module AridCache
2
2
  class Store < Hash
3
- extend ActiveSupport::Memoizable
3
+ Struct.new('Item', :cache_key, :proc, :klass, :opts)
4
4
 
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
5
+ def query(key, opts, object, &block)
6
+ return store(object, key, Proc.new, opts) if block_given? # store a proc
10
7
 
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
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))
37
17
  else
38
- self['proc']
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)
23
+ else
24
+ raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create query.")
39
25
  end
40
- end
26
+ end
41
27
  end
42
28
 
43
- def has?(object, key)
44
- self.include?(object_store_key(object, key))
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!)
45
33
  end
46
34
 
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
-
56
35
  def find(object, 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)
36
+ self[object.arid_cache_key(key)]
63
37
  end
64
38
 
39
+ # Find or dynamically create a proc
65
40
  def find_or_create(object, key)
66
- store_key = object_store_key(object, key)
67
- if self.include?(store_key)
68
- self[store_key]
41
+ cache_key = object.arid_cache_key(key)
42
+ if include?(cache_key)
43
+ self[cache_key]
69
44
  else
70
- self[store_key] = AridCache::Store::Blueprint.new(key, object)
45
+ self[cache_key] = Struct::Item.new(cache_key, Proc.new { object.send(key) }, (object.is_a?(Class) ? object : object.class))
71
46
  end
72
47
  end
73
48
 
74
- protected
75
-
76
- def initialize
49
+ def has?(object, key)
50
+ self.include?(object.arid_cache_key(key))
77
51
  end
78
52
 
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
53
+ # Empty the proc store
54
+ def delete!
55
+ delete_if { true }
56
+ end
83
57
  end
84
58
  end
data/lib/arid_cache.rb CHANGED
@@ -1,47 +1,15 @@
1
- dir = File.dirname(__FILE__)
2
- $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
3
-
4
- require 'arid_cache/helpers'
5
1
  require 'arid_cache/store'
6
2
  require 'arid_cache/active_record'
7
3
  require 'arid_cache/cache_proxy'
8
4
 
9
5
  module AridCache
10
- extend AridCache::Helpers
11
6
  class Error < StandardError; end
12
7
 
13
8
  def self.cache
14
- AridCache::CacheProxy
9
+ AridCache::CacheProxy.instance
15
10
  end
16
11
 
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.
36
12
  def self.included(base)
37
13
  base.send(:include, AridCache::ActiveRecord)
38
14
  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
47
15
  end
data/rails/init.rb CHANGED
@@ -1 +1 @@
1
- Kernel.load File.join(File.dirname(__FILE__), '..', 'init.rb')
1
+ require 'arid_cache'
data/rails/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
@@ -3,44 +3,37 @@ 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!
7
6
  get_user
8
7
  end
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
-
8
+
15
9
  test "should respond to methods" do
16
- assert User.respond_to?(:clear_cache)
17
- assert User.first.respond_to?(:clear_cache)
18
- assert_instance_of AridCache::Store, AridCache.store
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
19
14
  end
20
15
 
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
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
28
21
  assert_nothing_raised { User.first.is_high? }
29
- assert User.first.is_high?
30
- end
22
+ assert User.first.is_high?
23
+ end
31
24
 
32
25
  test "should allow me to cache on the model" do
33
26
  assert_nothing_raised do
34
27
  define_model_cache(User)
35
28
  end
36
- #assert_instance_of(Proc, AridCache.store[User.arid_cache_key('companies')].proc)
29
+ assert_instance_of(Proc, User.cache_store[User.arid_cache_key('companies')].proc)
37
30
  end
38
31
 
39
32
  test "should allow me to cache on the instance" do
40
33
  assert_nothing_raised do
41
34
  define_instance_cache(@user)
42
35
  end
43
- #assert_instance_of(Proc, AridCache.store[AridCache.store.store_key@user.arid_cache_key('companies')].proc)
36
+ assert_instance_of(Proc, @user.cache_store[@user.arid_cache_key('companies')].proc)
44
37
  end
45
38
 
46
39
  test "should raise an error on invalid dynamic caches" do
@@ -51,7 +44,7 @@ class AridCacheTest < ActiveSupport::TestCase
51
44
 
52
45
  test "should create dynamic caches given valid arguments" do
53
46
  assert_nothing_raised { @user.cached_companies }
54
- #assert_instance_of(Proc, AridCache.store[@user.arid_cache_key('companies')].proc)
47
+ assert_instance_of(Proc, @user.cache_store[@user.arid_cache_key('companies')].proc)
55
48
  end
56
49
 
57
50
  test "counts queries correctly" do
@@ -65,9 +58,9 @@ class AridCacheTest < ActiveSupport::TestCase
65
58
  end
66
59
 
67
60
  test "paginates results" do
68
- results = @user.cached_companies(:page => 1, :per_page => 3)
61
+ results = @user.cached_companies(:page => 1)
69
62
  assert_kind_of WillPaginate::Collection, results
70
- assert_equal 3, results.size
63
+ assert_equal 2, results.size
71
64
  assert_equal @user.companies.count, results.total_entries
72
65
  assert_equal 1, results.current_page
73
66
  end
@@ -82,16 +75,15 @@ class AridCacheTest < ActiveSupport::TestCase
82
75
  test "works for different pages" do
83
76
  results = @user.cached_companies(:page => 2, :per_page => 3)
84
77
  assert_kind_of WillPaginate::Collection, results
85
- assert results.size <= 3
86
- assert_equal @user.companies.count, results.total_entries
87
- assert_equal 2, results.current_page
78
+ assert_equal (@user.companies.count-3)%3, results.size
79
+ assert_equal @user.companies.count, results.total_entries
88
80
  end
89
-
81
+
90
82
  test "ignores random parameters" do
91
83
  result = @user.cached_companies(:invalid => :params, 'random' => 'values', :user_id => 3)
92
84
  assert_equal @user.companies, result
93
85
  end
94
-
86
+
95
87
  test "passes on options to find" do
96
88
  actual = @user.cached_companies(:order => 'users.id DESC')
97
89
  expected = @user.companies
@@ -107,101 +99,31 @@ class AridCacheTest < ActiveSupport::TestCase
107
99
  end
108
100
 
109
101
  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
154
102
  assert_queries(1) do
155
- User.cached_companies(:page => 1)
103
+ assert_equal 5, @user.cached_companies_count
104
+ assert_equal 5, @user.cached_companies_count
156
105
  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
165
106
  assert_queries(1) do
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
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
200
121
 
201
122
  protected
202
123
 
203
124
  def get_user
204
125
  @user = User.first
126
+ @user.cache_store.delete!
205
127
  @user.clear_cache
206
128
  define_instance_cache(@user)
207
129
  @user
@@ -219,13 +141,13 @@ class AridCacheTest < ActiveSupport::TestCase
219
141
  end
220
142
 
221
143
  def define_instance_cache(user)
222
- user.cache_companies(:per_page => 2) do
144
+ user.cached_companies(:per_page => 2) do
223
145
  user.companies
224
146
  end
225
147
  end
226
148
 
227
149
  def define_model_cache(model)
228
- model.cache_companies(:per_page => 2) do
150
+ model.cached_companies(:per_page => 2) do
229
151
  model.companies
230
152
  end
231
153
  end
data/test/db/schema.rb CHANGED
@@ -8,6 +8,5 @@ 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
12
11
  end
13
12
  end
@@ -1,6 +1,5 @@
1
- <% for digit in 1..15 %>
1
+ <% for digit in 1..5 %>
2
2
  acme_<%= digit %>:
3
3
  name: Acme <%= digit %>
4
- owner_id: <%= rand(10) %>
5
- employees: <%= rand(200) %>
4
+ owner_id: 1
6
5
  <% end %>
@@ -1,5 +1,3 @@
1
- <% for digit in 1..10 %>
2
- user_<%= digit %>:
3
- name: Bob <%= digit %>
4
- email: bob<%= digit %>@ibm.com
5
- <% end %>
1
+ bob:
2
+ name: Bob
3
+ email: bob@ibm.com
data/test/models/user.rb CHANGED
@@ -1,17 +1,9 @@
1
1
  require 'arid_cache'
2
2
 
3
3
  class User < ActiveRecord::Base
4
+ include AridCache
4
5
  has_many :companies, :foreign_key => :owner_id
5
6
  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
15
7
 
16
8
  def method_missing(method, *args)
17
9
  if method == :is_high?
data/test/test_helper.rb CHANGED
@@ -7,11 +7,7 @@ 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
13
10
  require 'arid_cache'
14
- AridCache.init_rails
15
11
 
16
12
  # Setup logging
17
13
  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.0.5
4
+ version: 0.1.0
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-25 00:00:00 +08:00
12
+ date: 2009-12-21 00:00:00 +08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -42,10 +42,7 @@ dependencies:
42
42
  - !ruby/object:Gem::Version
43
43
  version: "0"
44
44
  version:
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
-
45
+ description: ""
49
46
  email: kjvarga@gmail.com
50
47
  executables: []
51
48
 
@@ -61,13 +58,13 @@ files:
61
58
  - Rakefile
62
59
  - VERSION
63
60
  - arid_cache.gemspec
64
- - init.rb
65
61
  - lib/arid_cache.rb
66
62
  - lib/arid_cache/active_record.rb
67
63
  - lib/arid_cache/cache_proxy.rb
68
- - lib/arid_cache/helpers.rb
69
64
  - lib/arid_cache/store.rb
70
65
  - rails/init.rb
66
+ - rails/install.rb
67
+ - rails/uninstall.rb
71
68
  - spec/arid_cache_spec.rb
72
69
  - spec/spec.opts
73
70
  - spec/spec_helper.rb
data/init.rb DELETED
@@ -1,6 +0,0 @@
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,86 +0,0 @@
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