djmaze-arid_cache 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +26 -0
- data/Gemfile +18 -0
- data/LICENSE +20 -0
- data/README.rdoc +394 -0
- data/Rakefile +78 -0
- data/VERSION +1 -0
- data/arid_cache.gemspec +104 -0
- data/init.rb +6 -0
- data/lib/arid_cache/active_record.rb +95 -0
- data/lib/arid_cache/cache_proxy.rb +368 -0
- data/lib/arid_cache/helpers.rb +86 -0
- data/lib/arid_cache/store.rb +125 -0
- data/lib/arid_cache.rb +47 -0
- data/rails/init.rb +1 -0
- data/spec/arid_cache/arid_cache_spec.rb +39 -0
- data/spec/arid_cache/cache_proxy_result_spec.rb +53 -0
- data/spec/arid_cache/cache_proxy_spec.rb +95 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/ar_query.rb +128 -0
- data/spec/support/custom_methods.rb +7 -0
- data/spec/support/matchers.rb +33 -0
- data/test/arid_cache_test.rb +414 -0
- data/test/console +15 -0
- data/test/lib/add_query_counting_to_active_record.rb +11 -0
- data/test/lib/blueprint.rb +29 -0
- data/test/lib/db_prepare.rb +34 -0
- data/test/lib/fix_active_support_file_store_expires_in.rb +18 -0
- data/test/lib/mock_rails.rb +29 -0
- data/test/lib/models/company.rb +6 -0
- data/test/lib/models/empty_user_relation.rb +5 -0
- data/test/lib/models/user.rb +32 -0
- data/test/log/.gitignore +0 -0
- data/test/test_helper.rb +19 -0
- metadata +177 -0
data/Rakefile
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
Bundler.require
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
gem.name = "djmaze-arid_cache"
|
9
|
+
gem.summary = %Q{Automates efficient caching of your ActiveRecord collections, gives you counts for free and supports pagination.}
|
10
|
+
gem.description = <<-END.gsub(/^\s+/, '')
|
11
|
+
Fork of arid_cache which defines caching methods once on a class instead of per object, thus preventing "singleton can't be dumped" from memcached!
|
12
|
+
|
13
|
+
AridCache makes caching easy and effective. AridCache supports caching on all your model named scopes, class methods and instance methods right out of the box. AridCache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
|
14
|
+
|
15
|
+
AridCache is designed for handling large, expensive ActiveRecord collections but is equally useful for caching anything else as well.
|
16
|
+
END
|
17
|
+
gem.email = "maze@strahlungsfrei.de"
|
18
|
+
gem.homepage = "http://github.com/djmaze/arid_cache"
|
19
|
+
gem.authors = ["Karl Varga", "Martin Honermeyer"]
|
20
|
+
gem.add_dependency "will_paginate"
|
21
|
+
gem.add_development_dependency "will_paginate"
|
22
|
+
gem.add_development_dependency "faker"
|
23
|
+
gem.add_development_dependency "machinist"
|
24
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
25
|
+
end
|
26
|
+
Jeweler::GemcutterTasks.new
|
27
|
+
rescue LoadError
|
28
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'spec/rake/spectask'
|
32
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
33
|
+
spec.libs << 'lib' << 'spec'
|
34
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
35
|
+
end
|
36
|
+
|
37
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
38
|
+
spec.libs << 'lib' << 'spec'
|
39
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
40
|
+
spec.rcov = true
|
41
|
+
end
|
42
|
+
|
43
|
+
task :spec => :check_dependencies
|
44
|
+
|
45
|
+
require 'rake/testtask'
|
46
|
+
Rake::TestTask.new(:test) do |test|
|
47
|
+
test.libs << 'lib' << 'test'
|
48
|
+
test.pattern = 'test/**/*_test.rb'
|
49
|
+
test.verbose = true
|
50
|
+
end
|
51
|
+
|
52
|
+
task :test => :check_dependencies
|
53
|
+
task :default => :test
|
54
|
+
|
55
|
+
require 'rake/rdoctask'
|
56
|
+
Rake::RDocTask.new do |rdoc|
|
57
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
58
|
+
|
59
|
+
rdoc.rdoc_dir = 'rdoc'
|
60
|
+
rdoc.title = "arid_cache #{version}"
|
61
|
+
rdoc.rdoc_files.include('README*')
|
62
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
63
|
+
end
|
64
|
+
|
65
|
+
namespace :release do
|
66
|
+
desc "Release a new patch version"
|
67
|
+
task :patch do
|
68
|
+
Rake::Task['version:bump:patch'].invoke
|
69
|
+
Rake::Task['release:current'].invoke
|
70
|
+
end
|
71
|
+
|
72
|
+
desc "Release the current version (after a manual version bump). This rebuilds the gemspec, pushes the updated code, tags it and releases to RubyGems"
|
73
|
+
task :current do
|
74
|
+
Rake::Task['github:release'].invoke
|
75
|
+
Rake::Task['git:release'].invoke
|
76
|
+
Rake::Task['gemcutter:release'].invoke
|
77
|
+
end
|
78
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.1.0
|
data/arid_cache.gemspec
ADDED
@@ -0,0 +1,104 @@
|
|
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 = "1.1.0"
|
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{2010-12-17}
|
13
|
+
s.description = %q{AridCache makes caching easy and effective. AridCache supports caching on all your model named scopes, class methods and instance methods right out of the box. AridCache prevents caching logic from cluttering your models and clarifies your logic by making explicit calls to cached result sets.
|
14
|
+
AridCache 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
|
+
"Gemfile",
|
24
|
+
"LICENSE",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"arid_cache.gemspec",
|
29
|
+
"init.rb",
|
30
|
+
"lib/arid_cache.rb",
|
31
|
+
"lib/arid_cache/active_record.rb",
|
32
|
+
"lib/arid_cache/cache_proxy.rb",
|
33
|
+
"lib/arid_cache/helpers.rb",
|
34
|
+
"lib/arid_cache/store.rb",
|
35
|
+
"rails/init.rb",
|
36
|
+
"spec/arid_cache/arid_cache_spec.rb",
|
37
|
+
"spec/arid_cache/cache_proxy_result_spec.rb",
|
38
|
+
"spec/arid_cache/cache_proxy_spec.rb",
|
39
|
+
"spec/spec.opts",
|
40
|
+
"spec/spec_helper.rb",
|
41
|
+
"spec/support/ar_query.rb",
|
42
|
+
"spec/support/custom_methods.rb",
|
43
|
+
"spec/support/matchers.rb",
|
44
|
+
"test/arid_cache_test.rb",
|
45
|
+
"test/console",
|
46
|
+
"test/lib/add_query_counting_to_active_record.rb",
|
47
|
+
"test/lib/blueprint.rb",
|
48
|
+
"test/lib/db_prepare.rb",
|
49
|
+
"test/lib/fix_active_support_file_store_expires_in.rb",
|
50
|
+
"test/lib/mock_rails.rb",
|
51
|
+
"test/lib/models/company.rb",
|
52
|
+
"test/lib/models/empty_user_relation.rb",
|
53
|
+
"test/lib/models/user.rb",
|
54
|
+
"test/log/.gitignore",
|
55
|
+
"test/test_helper.rb"
|
56
|
+
]
|
57
|
+
s.homepage = %q{http://github.com/kjvarga/arid_cache}
|
58
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
59
|
+
s.require_paths = ["lib"]
|
60
|
+
s.rubygems_version = %q{1.3.7}
|
61
|
+
s.summary = %q{Automates efficient caching of your ActiveRecord collections, gives you counts for free and supports pagination.}
|
62
|
+
s.test_files = [
|
63
|
+
"spec/arid_cache/arid_cache_spec.rb",
|
64
|
+
"spec/arid_cache/cache_proxy_result_spec.rb",
|
65
|
+
"spec/arid_cache/cache_proxy_spec.rb",
|
66
|
+
"spec/spec_helper.rb",
|
67
|
+
"spec/support/ar_query.rb",
|
68
|
+
"spec/support/custom_methods.rb",
|
69
|
+
"spec/support/matchers.rb",
|
70
|
+
"test/arid_cache_test.rb",
|
71
|
+
"test/lib/add_query_counting_to_active_record.rb",
|
72
|
+
"test/lib/blueprint.rb",
|
73
|
+
"test/lib/db_prepare.rb",
|
74
|
+
"test/lib/fix_active_support_file_store_expires_in.rb",
|
75
|
+
"test/lib/mock_rails.rb",
|
76
|
+
"test/lib/models/company.rb",
|
77
|
+
"test/lib/models/empty_user_relation.rb",
|
78
|
+
"test/lib/models/user.rb",
|
79
|
+
"test/test_helper.rb"
|
80
|
+
]
|
81
|
+
|
82
|
+
if s.respond_to? :specification_version then
|
83
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
84
|
+
s.specification_version = 3
|
85
|
+
|
86
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
87
|
+
s.add_runtime_dependency(%q<will_paginate>, [">= 0"])
|
88
|
+
s.add_development_dependency(%q<will_paginate>, [">= 0"])
|
89
|
+
s.add_development_dependency(%q<faker>, [">= 0"])
|
90
|
+
s.add_development_dependency(%q<machinist>, [">= 0"])
|
91
|
+
else
|
92
|
+
s.add_dependency(%q<will_paginate>, [">= 0"])
|
93
|
+
s.add_dependency(%q<will_paginate>, [">= 0"])
|
94
|
+
s.add_dependency(%q<faker>, [">= 0"])
|
95
|
+
s.add_dependency(%q<machinist>, [">= 0"])
|
96
|
+
end
|
97
|
+
else
|
98
|
+
s.add_dependency(%q<will_paginate>, [">= 0"])
|
99
|
+
s.add_dependency(%q<will_paginate>, [">= 0"])
|
100
|
+
s.add_dependency(%q<faker>, [">= 0"])
|
101
|
+
s.add_dependency(%q<machinist>, [">= 0"])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
data/init.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
module AridCache
|
2
|
+
module ActiveRecord
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.extend MirrorMethods
|
6
|
+
base.send :include, MirrorMethods
|
7
|
+
base.class_eval do
|
8
|
+
alias_method_chain :method_missing, :arid_cache
|
9
|
+
alias_method_chain :respond_to?, :arid_cache
|
10
|
+
end
|
11
|
+
class << base
|
12
|
+
alias_method_chain :method_missing, :arid_cache
|
13
|
+
alias_method_chain :respond_to?, :arid_cache
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module MirrorMethods
|
18
|
+
def clear_caches
|
19
|
+
AridCache.cache.clear_class_caches(self)
|
20
|
+
AridCache.cache.clear_instance_caches(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear_class_caches
|
24
|
+
AridCache.cache.clear_class_caches(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear_instance_caches
|
28
|
+
AridCache.cache.clear_instance_caches(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return an AridCache key for the given key fragment for this object.
|
32
|
+
#
|
33
|
+
# Supported options:
|
34
|
+
# :auto_expire => true/false # (default false) whether or not to use the <tt>cache_key</tt> method on instance caches
|
35
|
+
#
|
36
|
+
# Examples:
|
37
|
+
# User.arid_cache_key('companies') => 'user-companies'
|
38
|
+
# User.first.arid_cache_key('companies') => 'users/1-companies'
|
39
|
+
# User.first.arid_cache_key('companies', :auto_expire => true) => 'users/20090120091123-companies'
|
40
|
+
#
|
41
|
+
def arid_cache_key(key, options={})
|
42
|
+
object_key = if self.is_a?(Class)
|
43
|
+
self.name.downcase
|
44
|
+
elsif options[:auto_expire]
|
45
|
+
self.cache_key
|
46
|
+
else
|
47
|
+
"#{self.class.name.downcase.pluralize}/#{self.id}"
|
48
|
+
end
|
49
|
+
'arid-cache-' + object_key + '-' + key.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
def respond_to_with_arid_cache?(method, include_private = false) #:nodoc:
|
53
|
+
if (method.to_s =~ /^cached_.*(_count)?$/).nil?
|
54
|
+
respond_to_without_arid_cache?(method, include_private)
|
55
|
+
elsif method.to_s =~ /^cached_(.*)_count$/
|
56
|
+
AridCache.store.has?(self, "#{$1}_count") || AridCache.store.has?(self, $1) || super("#{$1}_count", include_private) || super($1, include_private)
|
57
|
+
elsif method.to_s =~ /^cached_(.*)$/
|
58
|
+
AridCache.store.has?(self, $1) || super($1, include_private)
|
59
|
+
else
|
60
|
+
respond_to_without_arid_cache?(method, include_private)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
def method_missing_with_arid_cache(method, *args, &block) #:nodoc:
|
67
|
+
if method.to_s =~ /^cached_(.*)$/
|
68
|
+
opts = args.empty? ? {} : args.first
|
69
|
+
AridCache.lookup(self, $1, opts, &block)
|
70
|
+
else
|
71
|
+
method_missing_without_arid_cache(method, *args)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module ClassMethods
|
77
|
+
|
78
|
+
def instance_caches(opts={}, &block)
|
79
|
+
AridCache::Store::InstanceCacheConfiguration.new(self, opts).instance_eval(&block) && nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def class_caches(opts={}, &block)
|
83
|
+
AridCache::Store::ClassCacheConfiguration.new(self, opts).instance_eval(&block) && nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_mysql_adapter?
|
87
|
+
@is_mysql_adapter ||= !!(::ActiveRecord::Base.connection.adapter_name =~ /MySQL/i)
|
88
|
+
end
|
89
|
+
|
90
|
+
def is_mysql_adapter=(value)
|
91
|
+
@is_mysql_adapter = value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,368 @@
|
|
1
|
+
module AridCache
|
2
|
+
class CacheProxy
|
3
|
+
attr_accessor :object, :key, :opts, :blueprint, :cached, :cache_key, :block, :records, :combined_options, :klass
|
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.class.name
|
21
|
+
end
|
22
|
+
|
23
|
+
def klass
|
24
|
+
self['klass'].constantize unless self['klass'].nil?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Managing your caches
|
30
|
+
#
|
31
|
+
|
32
|
+
def self.clear_caches
|
33
|
+
Rails.cache.delete_matched(%r[arid-cache-.*])
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.clear_class_caches(object)
|
37
|
+
key = (object.is_a?(Class) ? object : object.class).name.downcase + '-'
|
38
|
+
Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.clear_instance_caches(object)
|
42
|
+
key = (object.is_a?(Class) ? object : object.class).name.pluralize.downcase
|
43
|
+
Rails.cache.delete_matched(%r[arid-cache-#{key}.*])
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(object, key, opts={}, &block)
|
47
|
+
self.object = object
|
48
|
+
self.key = key
|
49
|
+
self.opts = opts.symbolize_keys
|
50
|
+
self.blueprint = AridCache.store.find(object, key)
|
51
|
+
self.block = block
|
52
|
+
self.records = nil
|
53
|
+
|
54
|
+
# The options from the blueprint merged with the options for this call
|
55
|
+
self.combined_options = self.blueprint.nil? ? self.opts : self.blueprint.opts.merge(self.opts)
|
56
|
+
self.cache_key = object.arid_cache_key(key, opts_for_cache_key)
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Fetching results
|
61
|
+
#
|
62
|
+
|
63
|
+
# Return a count of ids in the cache, or return whatever is in the cache if it is
|
64
|
+
# not a CacheProxy::Result
|
65
|
+
def fetch_count
|
66
|
+
if refresh_cache?
|
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
|
+
# Return a list of records using the options provided. If the item in the cache
|
80
|
+
# is not a CacheProxy::Result it is returned as-is. If there is nothing in the cache
|
81
|
+
# the block defining the cache is exectued. If the :raw option is true, returns the
|
82
|
+
# CacheProxy::Result unmodified, ignoring other options, except where those options
|
83
|
+
# are used to initialize the cache.
|
84
|
+
def fetch
|
85
|
+
@raw_result = opts_for_cache_proxy[:raw] == true
|
86
|
+
|
87
|
+
result = if refresh_cache?
|
88
|
+
execute_find(@raw_result)
|
89
|
+
elsif cached.is_a?(AridCache::CacheProxy::Result)
|
90
|
+
if cached.has_ids? && @raw_result
|
91
|
+
self.cached # return it unmodified
|
92
|
+
elsif cached.has_ids?
|
93
|
+
fetch_from_cache # return a list of active records after applying options
|
94
|
+
else # true if we have only calculated the count thus far
|
95
|
+
execute_find(@raw_result)
|
96
|
+
end
|
97
|
+
else
|
98
|
+
cached # some base type, return it unmodified
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Clear the cached result for this cache only
|
103
|
+
def clear_cached
|
104
|
+
Rails.cache.delete(self.cache_key, opts_for_cache)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return the cached result for this object's key
|
108
|
+
def cached
|
109
|
+
@cached ||= Rails.cache.read(self.cache_key, opts_for_cache)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Return the class of the cached results i.e. if the cached result is a
|
113
|
+
# list of Album records, then klass returns Album. If there is nothing
|
114
|
+
# in the cache, then the class is inferred to be the class of the object
|
115
|
+
# that the cached method is being called on.
|
116
|
+
def klass
|
117
|
+
@klass ||= if self.cached && self.cached.is_a?(AridCache::CacheProxy::Result)
|
118
|
+
self.cached.klass
|
119
|
+
else
|
120
|
+
object_base_class
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
# Return a list of records from the database using the ids from
|
128
|
+
# the cached Result.
|
129
|
+
#
|
130
|
+
# The result is paginated if the :page option is preset, otherwise
|
131
|
+
# a regular list of ActiveRecord results is returned.
|
132
|
+
#
|
133
|
+
# If no :order is specified, the current ordering of the ids is
|
134
|
+
# preserved with some fancy SQL.
|
135
|
+
def fetch_from_cache
|
136
|
+
if paginate?
|
137
|
+
|
138
|
+
# Return a paginated collection
|
139
|
+
if cached.ids.empty?
|
140
|
+
|
141
|
+
# No ids, return an empty WillPaginate result
|
142
|
+
[].paginate(opts_for_paginate)
|
143
|
+
|
144
|
+
elsif combined_options.include?(:order)
|
145
|
+
|
146
|
+
# An order has been specified. We have to go to the database
|
147
|
+
# and paginate there because the contents of the requested
|
148
|
+
# page will be different.
|
149
|
+
klass.paginate(cached.ids, { :total_entries => cached.ids.size }.merge(opts_for_find.merge(opts_for_paginate)))
|
150
|
+
|
151
|
+
else
|
152
|
+
|
153
|
+
# Order is unchanged. We can paginate in memory and only select
|
154
|
+
# those ids that we actually need. This is the most efficient.
|
155
|
+
paged_ids = cached.ids.paginate(opts_for_paginate)
|
156
|
+
paged_ids.replace(klass.find_all_by_id(paged_ids, opts_for_find(paged_ids)))
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
elsif cached.ids.empty?
|
161
|
+
|
162
|
+
# We are returning a regular (non-paginated) result.
|
163
|
+
# If we don't have any ids, that's an empty result
|
164
|
+
# in any language.
|
165
|
+
[]
|
166
|
+
|
167
|
+
elsif combined_options.include?(:order)
|
168
|
+
|
169
|
+
# An order has been specified, so use it.
|
170
|
+
klass.find_all_by_id(cached.ids, opts_for_find)
|
171
|
+
|
172
|
+
else
|
173
|
+
|
174
|
+
# No order has been specified, so we have to maintain
|
175
|
+
# the current order. We do this by passing some extra
|
176
|
+
# SQL which orders by the current array ordering.
|
177
|
+
offset, limit = combined_options.delete(:offset) || 0, combined_options.delete(:limit) || cached.count
|
178
|
+
ids = cached.ids[offset, limit]
|
179
|
+
|
180
|
+
klass.find_all_by_id(ids, opts_for_find(ids))
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def paginate?
|
185
|
+
combined_options.include?(:page)
|
186
|
+
end
|
187
|
+
|
188
|
+
def refresh_cache?
|
189
|
+
cached.nil? || opts[:force]
|
190
|
+
end
|
191
|
+
|
192
|
+
def get_records
|
193
|
+
block = self.block || (blueprint && blueprint.proc)
|
194
|
+
self.records = block.nil? ? object.instance_eval(key) : object.instance_eval(&block)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Seed the cache by executing the stored block (or by calling a method on the object).
|
198
|
+
# Then apply any options like pagination or ordering before returning the result, which
|
199
|
+
# is either some base type, or usually, a list of active records.
|
200
|
+
#
|
201
|
+
# Options:
|
202
|
+
# raw - if true, return the CacheProxy::Result after seeding the cache, ignoring
|
203
|
+
# other options. Default is false.
|
204
|
+
def execute_find(raw = false)
|
205
|
+
get_records
|
206
|
+
cached = AridCache::CacheProxy::Result.new
|
207
|
+
|
208
|
+
if !records.is_a?(Enumerable) || (!records.empty? && !records.first.is_a?(::ActiveRecord::Base))
|
209
|
+
cached = records # some base type, cache it as itself
|
210
|
+
else
|
211
|
+
cached.ids = records.collect(&:id)
|
212
|
+
cached.count = records.size
|
213
|
+
if records.respond_to?(:proxy_reflection) # association proxy
|
214
|
+
cached.klass = records.proxy_reflection.klass
|
215
|
+
elsif !records.empty?
|
216
|
+
cached.klass = records.first.class
|
217
|
+
else
|
218
|
+
cached.klass = object_base_class
|
219
|
+
end
|
220
|
+
end
|
221
|
+
Rails.cache.write(cache_key, cached, opts_for_cache)
|
222
|
+
self.cached = cached
|
223
|
+
|
224
|
+
# Return the raw result?
|
225
|
+
return self.cached if raw
|
226
|
+
|
227
|
+
# An order has been specified. We have to go to the database
|
228
|
+
# to order because we can't be sure that the current order is the same as the cache.
|
229
|
+
if cached.is_a?(AridCache::CacheProxy::Result) && combined_options.include?(:order)
|
230
|
+
self.klass = self.cached.klass # TODO used by fetch_from_cache needs refactor
|
231
|
+
fetch_from_cache
|
232
|
+
else
|
233
|
+
process_result_in_memory(records)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Convert records to an array before calling paginate. If we don't do this
|
238
|
+
# and the result is a named scope, paginate will trigger an additional query
|
239
|
+
# to load the page rather than just using the records we have already fetched.
|
240
|
+
#
|
241
|
+
# If we are not paginating and the options include :limit (and optionally :offset)
|
242
|
+
# apply the limit and offset to the records before returning them.
|
243
|
+
#
|
244
|
+
# Otherwise we have an issue where all the records are returned the first time
|
245
|
+
# the collection is loaded, but on subsequent calls the options_for_find are
|
246
|
+
# included and you get different results. Note that with options like :order
|
247
|
+
# this cannot be helped. We don't want to modify the query that generates the
|
248
|
+
# collection because the idea is to allow getting different perspectives of the
|
249
|
+
# cached collection without relying on modifying the collection as a whole.
|
250
|
+
#
|
251
|
+
# If you do want a specialized, modified, or subset of the collection it's best
|
252
|
+
# to define it in a block and have a new cache for it:
|
253
|
+
#
|
254
|
+
# model.my_special_collection { the_collection(:order => 'new order', :limit => 10) }
|
255
|
+
def process_result_in_memory(records)
|
256
|
+
if opts.include?(:page)
|
257
|
+
records = records.respond_to?(:to_a) ? records.to_a : records
|
258
|
+
records.respond_to?(:paginate) ? records.paginate(opts_for_paginate) : records
|
259
|
+
elsif opts.include?(:limit)
|
260
|
+
records = records.respond_to?(:to_a) ? records.to_a : records
|
261
|
+
offset = opts[:offset] || 0
|
262
|
+
records[offset, opts[:limit]]
|
263
|
+
else
|
264
|
+
records
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def execute_count
|
269
|
+
get_records
|
270
|
+
cached = AridCache::CacheProxy::Result.new
|
271
|
+
|
272
|
+
# Just get the count if we can.
|
273
|
+
#
|
274
|
+
# Because of how AssociationProxy works, if we even look at it, it'll
|
275
|
+
# trigger the query. So don't look.
|
276
|
+
#
|
277
|
+
# Association proxy or named scope. Check for an association first, because
|
278
|
+
# it doesn't trigger the select if it's actually named scope. Calling respond_to?
|
279
|
+
# on an association proxy will hower trigger a select because it loads up the target
|
280
|
+
# and passes the respond_to? on to it.
|
281
|
+
if records.respond_to?(:proxy_reflection) || records.respond_to?(:proxy_options)
|
282
|
+
cached.count = records.count # just get the count
|
283
|
+
cached.klass = object_base_class
|
284
|
+
elsif records.is_a?(Enumerable) && (records.empty? || records.first.is_a?(::ActiveRecord::Base))
|
285
|
+
cached.ids = records.collect(&:id) # get everything now that we have it
|
286
|
+
cached.count = records.size
|
287
|
+
cached.klass = records.empty? ? object_base_class : records.first.class
|
288
|
+
else
|
289
|
+
cached = records # some base type, cache it as itself
|
290
|
+
end
|
291
|
+
|
292
|
+
Rails.cache.write(cache_key, cached, opts_for_cache)
|
293
|
+
self.cached = cached
|
294
|
+
cached.respond_to?(:count) ? cached.count : cached
|
295
|
+
end
|
296
|
+
|
297
|
+
OPTIONS_FOR_PAGINATE = [:page, :per_page, :total_entries, :finder]
|
298
|
+
|
299
|
+
# Filter options for paginate, if *klass* is set, we get the :per_page value from it.
|
300
|
+
def opts_for_paginate
|
301
|
+
paginate_opts = combined_options.reject { |k,v| !OPTIONS_FOR_PAGINATE.include?(k) }
|
302
|
+
paginate_opts[:finder] = :find_all_by_id unless paginate_opts.include?(:finder)
|
303
|
+
paginate_opts[:per_page] = klass.per_page if klass && !paginate_opts.include?(:per_page)
|
304
|
+
paginate_opts
|
305
|
+
end
|
306
|
+
|
307
|
+
OPTIONS_FOR_FIND = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock ]
|
308
|
+
|
309
|
+
# Preserve the original order of the results if no :order option is specified.
|
310
|
+
#
|
311
|
+
# @arg ids array of ids to order by unless an :order option is specified. If not
|
312
|
+
# specified, cached.ids is used.
|
313
|
+
def opts_for_find(ids=nil)
|
314
|
+
ids ||= cached.ids
|
315
|
+
find_opts = combined_options.reject { |k,v| !OPTIONS_FOR_FIND.include?(k) }
|
316
|
+
find_opts[:order] = preserve_order(ids) unless find_opts.include?(:order)
|
317
|
+
find_opts
|
318
|
+
end
|
319
|
+
|
320
|
+
OPTIONS_FOR_CACHE = [ :expires_in ]
|
321
|
+
|
322
|
+
def opts_for_cache
|
323
|
+
combined_options.reject { |k,v| !OPTIONS_FOR_CACHE.include?(k) }
|
324
|
+
end
|
325
|
+
|
326
|
+
OPTIONS_FOR_CACHE_KEY = [ :auto_expire ]
|
327
|
+
|
328
|
+
def opts_for_cache_key
|
329
|
+
combined_options.reject { |k,v| !OPTIONS_FOR_CACHE_KEY.include?(k) }
|
330
|
+
end
|
331
|
+
|
332
|
+
OPTIONS_FOR_CACHE_PROXY = [:raw, :clear]
|
333
|
+
|
334
|
+
# Returns options that affect the cache proxy result
|
335
|
+
def opts_for_cache_proxy
|
336
|
+
combined_options.reject { |k,v| !OPTIONS_FOR_CACHE_PROXY.include?(k) }
|
337
|
+
end
|
338
|
+
|
339
|
+
def object_base_class #:nodoc:
|
340
|
+
object.is_a?(Class) ? object : object.class
|
341
|
+
end
|
342
|
+
|
343
|
+
# Generate an ORDER BY clause that preserves the ordering of the ids in *ids*.
|
344
|
+
#
|
345
|
+
# The method we use depends on the database adapter because only MySQL
|
346
|
+
# supports the ORDER BY FIELD() function. For other databases we use
|
347
|
+
# a CASE statement.
|
348
|
+
#
|
349
|
+
# TODO: is it quicker to sort in memory?
|
350
|
+
def preserve_order(ids)
|
351
|
+
column = if self.klass.respond_to?(:table_name)
|
352
|
+
::ActiveRecord::Base.connection.quote_table_name(self.klass.table_name) + '.id'
|
353
|
+
else
|
354
|
+
"id"
|
355
|
+
end
|
356
|
+
|
357
|
+
if ids.empty?
|
358
|
+
nil
|
359
|
+
elsif ::ActiveRecord::Base.is_mysql_adapter?
|
360
|
+
"FIELD(#{column},#{ids.join(',')})"
|
361
|
+
else
|
362
|
+
order = ''
|
363
|
+
ids.each_index { |i| order << "WHEN #{column}=#{ids[i]} THEN #{i+1} " }
|
364
|
+
"CASE " + order + " END"
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|