arid_cache 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +121 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/arid_cache.gemspec +85 -0
- data/init.rb +6 -0
- data/lib/arid_cache/active_record.rb +67 -0
- data/lib/arid_cache/cache_proxy.rb +209 -0
- data/lib/arid_cache/helpers.rb +86 -0
- data/lib/arid_cache/store.rb +84 -0
- data/lib/arid_cache.rb +47 -0
- data/rails/init.rb +1 -0
- data/spec/arid_cache_spec.rb +7 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- data/tasks/arid_cache_tasks.rake +4 -0
- data/test/arid_cache_test.rb +232 -0
- data/test/console +8 -0
- data/test/db/prepare.rb +34 -0
- data/test/db/schema.rb +13 -0
- data/test/fixtures/companies.yml +6 -0
- data/test/fixtures/users.yml +5 -0
- data/test/log/.gitignore +0 -0
- data/test/models/company.rb +5 -0
- data/test/models/user.rb +23 -0
- data/test/test_helper.rb +48 -0
- metadata +121 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Karl Varga
|
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,121 @@
|
|
1
|
+
== ARID Cache
|
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.
|
4
|
+
|
5
|
+
ARID Cache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
|
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:
|
48
|
+
|
49
|
+
class User < ActiveRecord::Base
|
50
|
+
include AridCache
|
51
|
+
has_many :pets
|
52
|
+
has_one :preferences
|
53
|
+
named_scope :active, :conditions => [ 'updated_at <= ', 5.minutes.ago ]
|
54
|
+
end
|
55
|
+
|
56
|
+
==== ARID Cache provides these methods:
|
57
|
+
|
58
|
+
User.cached_active # caches the user IDs and the count
|
59
|
+
User.cached_active_count # gets the count for free
|
60
|
+
|
61
|
+
user.cached_pets # caches the pets IDs and the count
|
62
|
+
user.cached_pets_count # gets the count for free
|
63
|
+
|
64
|
+
When we call these methods again, instead of doing a full select - usually including
|
65
|
+
complex joins or over very large tables which makes this expensive - it just
|
66
|
+
selects where the IDs are the cached IDs.
|
67
|
+
|
68
|
+
It also gives you paging using WillPaginate. The IDs from the cache are paginated and
|
69
|
+
only that page is selected from the database - again directly from the table, without
|
70
|
+
any complex joins.
|
71
|
+
|
72
|
+
==== Some examples of pagination:
|
73
|
+
|
74
|
+
User.cached_active.paginate(:page => 1, :per_page => 30)
|
75
|
+
User.cached_active.paginate(:page => 1)
|
76
|
+
User.cached_active.paginate(:page => 3)
|
77
|
+
|
78
|
+
You can also include options for find, such as <tt>:join</tt>, <tt>:include</tt> and <tt>order</tt>...basically any options that find supports.
|
79
|
+
|
80
|
+
User.cached_active.paginate(:page => 1, :include => :preferences)
|
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)
|
87
|
+
|
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
|
105
|
+
|
106
|
+
More docs to come...
|
107
|
+
|
108
|
+
== Note on Patches/Pull Requests
|
109
|
+
|
110
|
+
* Fork the project.
|
111
|
+
* Make your feature addition or bug fix.
|
112
|
+
* Add tests for it. This is important so I don't break it in a
|
113
|
+
future version unintentionally.
|
114
|
+
* Commit, do not mess with rakefile, version, or history.
|
115
|
+
(if you want to have your own version, that is fine but
|
116
|
+
bump version in a commit by itself I can ignore when I pull)
|
117
|
+
* Send me a pull request. Bonus points for topic branches.
|
118
|
+
|
119
|
+
== Copyright
|
120
|
+
|
121
|
+
Copyright (c) 2009 Karl Varga. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "arid_cache"
|
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
|
14
|
+
gem.email = "kjvarga@gmail.com"
|
15
|
+
gem.homepage = "http://github.com/kjvarga/arid_cache"
|
16
|
+
gem.authors = ["Karl Varga"]
|
17
|
+
gem.add_dependency "will_paginate"
|
18
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
19
|
+
gem.add_development_dependency "will_paginate"
|
20
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'spec/rake/spectask'
|
28
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
34
|
+
spec.libs << 'lib' << 'spec'
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :spec => :check_dependencies
|
40
|
+
|
41
|
+
task :default => :spec
|
42
|
+
|
43
|
+
require 'rake/rdoctask'
|
44
|
+
Rake::RDocTask.new do |rdoc|
|
45
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "arid_cache #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.5
|
data/arid_cache.gemspec
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{arid_cache}
|
8
|
+
s.version = "0.0.5"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
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
|
+
}
|
16
|
+
s.email = %q{kjvarga@gmail.com}
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".gitignore",
|
23
|
+
"LICENSE",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"arid_cache.gemspec",
|
28
|
+
"init.rb",
|
29
|
+
"lib/arid_cache.rb",
|
30
|
+
"lib/arid_cache/active_record.rb",
|
31
|
+
"lib/arid_cache/cache_proxy.rb",
|
32
|
+
"lib/arid_cache/helpers.rb",
|
33
|
+
"lib/arid_cache/store.rb",
|
34
|
+
"rails/init.rb",
|
35
|
+
"spec/arid_cache_spec.rb",
|
36
|
+
"spec/spec.opts",
|
37
|
+
"spec/spec_helper.rb",
|
38
|
+
"tasks/arid_cache_tasks.rake",
|
39
|
+
"test/arid_cache_test.rb",
|
40
|
+
"test/console",
|
41
|
+
"test/db/prepare.rb",
|
42
|
+
"test/db/schema.rb",
|
43
|
+
"test/fixtures/companies.yml",
|
44
|
+
"test/fixtures/users.yml",
|
45
|
+
"test/log/.gitignore",
|
46
|
+
"test/models/company.rb",
|
47
|
+
"test/models/user.rb",
|
48
|
+
"test/test_helper.rb"
|
49
|
+
]
|
50
|
+
s.homepage = %q{http://github.com/kjvarga/arid_cache}
|
51
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
52
|
+
s.require_paths = ["lib"]
|
53
|
+
s.rubygems_version = %q{1.3.5}
|
54
|
+
s.summary = %q{Automates efficient caching of your ActiveRecord collections, gives you counts for free and supports pagination.}
|
55
|
+
s.test_files = [
|
56
|
+
"spec/arid_cache_spec.rb",
|
57
|
+
"spec/spec_helper.rb",
|
58
|
+
"test/arid_cache_test.rb",
|
59
|
+
"test/db/prepare.rb",
|
60
|
+
"test/db/schema.rb",
|
61
|
+
"test/models/company.rb",
|
62
|
+
"test/models/user.rb",
|
63
|
+
"test/test_helper.rb"
|
64
|
+
]
|
65
|
+
|
66
|
+
if s.respond_to? :specification_version then
|
67
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
68
|
+
s.specification_version = 3
|
69
|
+
|
70
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
71
|
+
s.add_runtime_dependency(%q<will_paginate>, [">= 0"])
|
72
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
73
|
+
s.add_development_dependency(%q<will_paginate>, [">= 0"])
|
74
|
+
else
|
75
|
+
s.add_dependency(%q<will_paginate>, [">= 0"])
|
76
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
77
|
+
s.add_dependency(%q<will_paginate>, [">= 0"])
|
78
|
+
end
|
79
|
+
else
|
80
|
+
s.add_dependency(%q<will_paginate>, [">= 0"])
|
81
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
82
|
+
s.add_dependency(%q<will_paginate>, [">= 0"])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
data/init.rb
ADDED
@@ -0,0 +1,67 @@
|
|
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
|
+
|
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
|
31
|
+
|
32
|
+
# Return a cache key for the given key e.g.
|
33
|
+
# 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
|
38
|
+
end
|
39
|
+
|
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
|
53
|
+
|
54
|
+
# 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)
|
61
|
+
else
|
62
|
+
method_missing_without_arid_cache(method, *args)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module AridCache
|
2
|
+
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
|
10
|
+
|
11
|
+
def has_count?
|
12
|
+
!count.nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_ids?
|
16
|
+
!ids.nil?
|
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
|
26
|
+
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
|
+
|
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
|
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
|
74
|
+
else
|
75
|
+
cached # what else can we do? return it
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def fetch
|
80
|
+
if cached.nil? || opts[:force]
|
81
|
+
execute_find
|
82
|
+
elsif cached.is_a?(AridCache::CacheProxy::Result)
|
83
|
+
if cached.has_ids? # paginate and fetch here
|
84
|
+
klass = find_class_of_results
|
85
|
+
if opts.include?(:page)
|
86
|
+
klass.paginate(cached.ids, opts_for_paginate)
|
87
|
+
else
|
88
|
+
klass.find(cached.ids, opts_for_find)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
execute_find
|
92
|
+
end
|
93
|
+
else
|
94
|
+
cached # some base type, return it
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
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
|
121
|
+
end
|
122
|
+
Rails.cache.write(cache_key, cached)
|
123
|
+
|
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
|
157
|
+
end
|
158
|
+
|
159
|
+
def execute_count
|
160
|
+
get_records
|
161
|
+
cached = AridCache::CacheProxy::Result.new
|
162
|
+
|
163
|
+
# Just get the count if we can.
|
164
|
+
#
|
165
|
+
# Because of how AssociationProxy works, if we even look at it, it'll
|
166
|
+
# trigger the query. So don't look.
|
167
|
+
#
|
168
|
+
# Association proxy or named scope. Check for an association first, because
|
169
|
+
# it doesn't trigger the select if it's actually named scope. Calling respond_to?
|
170
|
+
# on an association proxy will hower trigger a select because it loads up the target
|
171
|
+
# and passes the respond_to? on to it.
|
172
|
+
if records.respond_to?(:proxy_reflection) || records.respond_to?(:proxy_options)
|
173
|
+
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))
|
176
|
+
cached.ids = records.collect(&:id) # get everything now that we have it
|
177
|
+
cached.count = records.size
|
178
|
+
cached.klass = records.empty? ? object_base_class : records.first.class
|
179
|
+
else
|
180
|
+
cached = records # some base type, cache it as itself
|
181
|
+
end
|
182
|
+
|
183
|
+
Rails.cache.write(cache_key, cached)
|
184
|
+
self.cached = cached
|
185
|
+
cached.respond_to?(:count) ? cached.count : cached
|
186
|
+
end
|
187
|
+
|
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
|
192
|
+
end
|
193
|
+
|
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
|
203
|
+
end
|
204
|
+
|
205
|
+
def find_class_of_results
|
206
|
+
opts[:class] || (blueprint && blueprint.opts[:class]) || cached.klass || object_base_class
|
207
|
+
end
|
208
|
+
end
|
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
|