static_record_cache 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +138 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/init.rb +2 -0
- data/install.rb +1 -0
- data/lib/acts_as_static_record.rb +517 -0
- data/lib/static_active_record_context.rb +60 -0
- data/lib/static_record_cache.rb +5 -0
- data/static_record_cache.gemspec +70 -0
- data/tasks/static_record_cache_tasks.rake +4 -0
- data/test/acts_as_static_record_test.rb +146 -0
- data/test/db/schema.rb +20 -0
- data/test/fixtures/carriers.yml +65 -0
- data/test/fixtures/phone_numbers.yml +36 -0
- data/test/models/carrier.rb +28 -0
- data/test/models/context_carrier.rb +5 -0
- data/test/models/phone_number.rb +5 -0
- data/test/static_active_record_context_test.rb +54 -0
- data/test/test_helper.rb +99 -0
- data/uninstall.rb +1 -0
- metadata +91 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Blythe Dunham
|
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,138 @@
|
|
1
|
+
= Static Record Cache
|
2
|
+
This plugin has two options for caching records of Static classes which change rarely or never
|
3
|
+
=acts_as_static_record
|
4
|
+
Permanently caches subclasses of ActiveRecord that contains data that changes rarely.
|
5
|
+
|
6
|
+
Includes support for:
|
7
|
+
* Find by Id, all, first (association lookups)
|
8
|
+
* Cached caluculations: <tt>Neighborhood.count</tt>s, sum, max, min
|
9
|
+
* Convenience lookups: <tt>Neighborhood[:seattle]</tt>
|
10
|
+
* Additional support for column name lookups: <tt>Neighborhood.find_by_name 'Seattle'</tt>
|
11
|
+
|
12
|
+
== Install
|
13
|
+
|
14
|
+
sudo gem install static_record_cache --source=http://gemcutter.org
|
15
|
+
|
16
|
+
script/plugin install git://github.com/blythedunham/static_record_cache.git
|
17
|
+
|
18
|
+
== Usage
|
19
|
+
class SomeMostlyStaticClass < ActiveRecord::Base
|
20
|
+
acts_as_static_record
|
21
|
+
end
|
22
|
+
|
23
|
+
Any finds that do not contain additional conditions, joins, and other arguments
|
24
|
+
become a cache call. One advantage over the query cache is that the static cache is searched
|
25
|
+
eliminating the need for +ActiveRecord+ to generate SQL
|
26
|
+
|
27
|
+
When a cache key is specified with option <tt>:key</tt>, additional
|
28
|
+
finder methods for ids and fields such as +find_by_id+ and +find_by_name_and_mother+
|
29
|
+
are overwritten to search the cache when no arguments (conditions) are specified.
|
30
|
+
If the cache key is not a column, then a finder method will be defined.
|
31
|
+
acts_as_static_record :key => :some_instance_method
|
32
|
+
Will define <tt>find_by_some_instance_method(value)</tt>
|
33
|
+
|
34
|
+
=== Options
|
35
|
+
* <tt>:key</tt> - a method or column of the instance used to specify a cache key. This should be unique.
|
36
|
+
* <tt>:find</tt> an additional find scope to specify <tt>:conditions</tt>,<tt>:joins</tt>, <tt>:select</tt>, <tt>:joins</ff> etc
|
37
|
+
* <tt>:find_by_attribute_support</tt> - set to true to add additional functionality for finders such as +find_by_id_and_name+ to use a cache search. This option is probably best for Rails 2.3
|
38
|
+
* <tt>:lookup_key</tt> - access the record from the class by a key name like <tt>User[:snowgiraffe]</tt>. <tt>:lookup_key</tt> is the column on which do do the lookup.
|
39
|
+
|
40
|
+
=== Examples
|
41
|
+
Caches on Id and telephone carrier name
|
42
|
+
class TelephoneCarrier < ActiveRecord::Base
|
43
|
+
acts_as_static_method :key => :name
|
44
|
+
end
|
45
|
+
|
46
|
+
Caches the WhiteList on phone_number_digits (in addition to ID)
|
47
|
+
create_table :sms_white_list, :force => true do |t|
|
48
|
+
t.column :phone_number_id, :integer, :null => false
|
49
|
+
t.column :notes, :string, :length => 100, :default => nil
|
50
|
+
end
|
51
|
+
|
52
|
+
class SmsWhiteList < ActiveRecord::Base
|
53
|
+
belongs_to :phone_number
|
54
|
+
|
55
|
+
acts_as_static_record :key => :phone_number_digits,
|
56
|
+
:find => :select => 'carriers.*, phone_number.number as phone_number_digits'
|
57
|
+
:joins => 'inner join phone_numbers on phone_numbers.carrier_id = carriers.id'
|
58
|
+
|
59
|
+
def phone_number_digits
|
60
|
+
self['phone_number_digits']||self.phone_number.number
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Direct cache hits
|
65
|
+
SmsWhiteList.find_by_phone_number_digits('12065551234')
|
66
|
+
SmsWhiteList.find_by_id(5)
|
67
|
+
SmsWhiteList.find :all
|
68
|
+
|
69
|
+
Searched cache hits
|
70
|
+
SmsWhiteList.find_by_notes('Some note')
|
71
|
+
|
72
|
+
==Calculation Support
|
73
|
+
Now with +calculate+ support for +sum+, +min+, and +max+ for integer columns and +count+ for all columns
|
74
|
+
Cache hits do not occur if options other than +distinct+ are used.
|
75
|
+
|
76
|
+
Cache hits:
|
77
|
+
Neighborhood.count
|
78
|
+
Neighborhood.count :name, :distinct => true
|
79
|
+
Neighborhood.sum :id
|
80
|
+
Neighborhood.max :id
|
81
|
+
Not cache hits:
|
82
|
+
Neighborhood.max :name
|
83
|
+
Neighborhood.count :zip_code, :conditions => ['name = ?', 'Seattle']
|
84
|
+
|
85
|
+
==Convenience lookup
|
86
|
+
Similar to acts_as_enumeration model returns the record where the
|
87
|
+
<tt>acts_as_static_record :key</tt> option matches +lookup+
|
88
|
+
If no key is specified, the primary_id column is used
|
89
|
+
|
90
|
+
class User < ActiveRecord::Base
|
91
|
+
acts_as_static_record :key => :user_name
|
92
|
+
end
|
93
|
+
|
94
|
+
Then later we can reference the objects by the user_name
|
95
|
+
User[:blythe]
|
96
|
+
User['snowgiraffe']
|
97
|
+
|
98
|
+
The key used will be the underscore version of the name. Punctuation marks
|
99
|
+
are removed. The following are equivalent:
|
100
|
+
User[:blythe_snowgiraffeawesome]
|
101
|
+
User['blythe-SNOWGIRaffe AWESome']
|
102
|
+
|
103
|
+
user = User.first
|
104
|
+
User[user.user_name] == user
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
= StaticActiveRecordContext
|
109
|
+
|
110
|
+
Extends the active_record_context plugin,
|
111
|
+
http://svn.techno-weenie.net/projects/plugins/active_record_context/
|
112
|
+
to permanently cache records for an ActiveRecord subclass
|
113
|
+
|
114
|
+
Finds on records IDS (as used by associations) will be cached for the life of the class.
|
115
|
+
This works both in and outside a with_context block.
|
116
|
+
|
117
|
+
== Example
|
118
|
+
|
119
|
+
class TelephoneCarriers < ActiveRecord::Base
|
120
|
+
extend StaticActiveRecordContext
|
121
|
+
end
|
122
|
+
|
123
|
+
phone_number.carrier
|
124
|
+
|
125
|
+
=== Developers
|
126
|
+
* Blythe Dunham http://snowgiraffe.com
|
127
|
+
|
128
|
+
=== Docs
|
129
|
+
* Rdoc: http://snowgiraffe.com/rdocs/static_record_cache/
|
130
|
+
=== Homepage
|
131
|
+
* Github Project: http://github.com/blythedunham/static_record_cache/tree/master
|
132
|
+
* Install: <tt>script/plugin install git://github.com/blythedunham/static_record_cache.git</tt>
|
133
|
+
|
134
|
+
|
135
|
+
|
136
|
+
|
137
|
+
|
138
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = "static_record_cache"
|
10
|
+
gem.summary = %Q{Permanently caches subclasses of ActiveRecord in memory. }
|
11
|
+
gem.description = %Q{Permanently caches subclasses of ActiveRecord in memory. }
|
12
|
+
gem.email = "blythe@snowgiraffe.com"
|
13
|
+
gem.homepage = "http://github.com/blythedunham/static_record_cache"
|
14
|
+
gem.authors = ["Blythe Dunham"]
|
15
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
16
|
+
# => gem.add_dependency 'activesupport'
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
desc 'Default: run unit tests.'
|
27
|
+
task :default => :test
|
28
|
+
|
29
|
+
desc 'Test the static_record_cache plugin.'
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = true
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'Generate documentation for the static_record_cache plugin.'
|
38
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
39
|
+
rdoc.rdoc_dir = 'rdoc'
|
40
|
+
rdoc.title = 'StaticRecordCache'
|
41
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
42
|
+
rdoc.rdoc_files.include('README')
|
43
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
44
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/init.rb
ADDED
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,517 @@
|
|
1
|
+
# =acts_as_static_record
|
2
|
+
# Permanently caches subclasses of ActiveRecord that contains data that changes rarely.
|
3
|
+
#
|
4
|
+
# Includes support for:
|
5
|
+
# * Find by Id, all, first (association lookups)
|
6
|
+
# * Cached caluculations: <tt>Neighborhood.count</tt>s, sum, max, min
|
7
|
+
# * Convenience lookups: <tt>Neighborhood[:seattle]</tt>
|
8
|
+
# * Additional support for column name lookups: <tt>Neighborhood.find_by_name 'Seattle'</tt>
|
9
|
+
#
|
10
|
+
# == Install
|
11
|
+
# script/plugin install git://github.com/blythedunham/static_record_cache.git
|
12
|
+
#
|
13
|
+
# == Usage
|
14
|
+
# class SomeMostlyStaticClass < ActiveRecord::Base
|
15
|
+
# acts_as_static_record
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Any finds that do not contain additional conditions, joins, and other arguments
|
19
|
+
# become a cache call. One advantage over the query cache is that the static cache is searched
|
20
|
+
# eliminating the need for +ActiveRecord+ to generate SQL
|
21
|
+
#
|
22
|
+
# When a cache key is specified with option <tt>:key</tt>, additional
|
23
|
+
# finder methods for ids and fields such as +find_by_id+ and +find_by_name_and_mother+
|
24
|
+
# are overwritten to search the cache when no arguments (conditions) are specified.
|
25
|
+
# If the cache key is not a column, then a finder method will be defined.
|
26
|
+
# acts_as_static_record :key => :some_instance_method
|
27
|
+
# Will define <tt>find_by_some_instance_method(value)</tt>
|
28
|
+
#
|
29
|
+
# === Options
|
30
|
+
# * <tt>:key</tt> - a method or column of the instance used to specify a cache key. This should be unique.
|
31
|
+
# * <tt>:find</tt> an additional find scope to specify <tt>:conditions</tt>,<tt>:joins</tt>, <tt>:select</tt>, <tt>:joins</ff> etc
|
32
|
+
# * <tt>:find_by_attribute_support</tt> - set to true to add additional functionality for finders such as +find_by_id_and_name+ to use a cache search. This option is probably best for Rails 2.3
|
33
|
+
# * <tt>:lookup_key</tt> - access the record from the class by a key name like <tt>User[:snowgiraffe]</tt>. <tt>:lookup_key</tt> is the column on which do do the lookup.
|
34
|
+
#
|
35
|
+
# === Examples
|
36
|
+
# Caches on Id and telephone carrier name
|
37
|
+
# class TelephoneCarrier < ActiveRecord::Base
|
38
|
+
# acts_as_static_method :key => :name
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# Caches the WhiteList on phone_number_digits (in addition to ID)
|
42
|
+
# create_table :sms_white_list, :force => true do |t|
|
43
|
+
# t.column :phone_number_id, :integer, :null => false
|
44
|
+
# t.column :notes, :string, :length => 100, :default => nil
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# class SmsWhiteList < ActiveRecord::Base
|
48
|
+
# belongs_to :phone_number
|
49
|
+
#
|
50
|
+
# acts_as_static_record :key => :phone_number_digits,
|
51
|
+
# :find => :select => 'carriers.*, phone_number.number as phone_number_digits'
|
52
|
+
# :joins => 'inner join phone_numbers on phone_numbers.carrier_id = carriers.id'
|
53
|
+
#
|
54
|
+
# def phone_number_digits
|
55
|
+
# self['phone_number_digits']||self.phone_number.number
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# Direct cache hits
|
60
|
+
# SmsWhiteList.find_by_phone_number_digits('12065551234')
|
61
|
+
# SmsWhiteList.find_by_id(5)
|
62
|
+
# SmsWhiteList.find :all
|
63
|
+
#
|
64
|
+
# Searched cache hits
|
65
|
+
# SmsWhiteList.find_by_notes('Some note')
|
66
|
+
#
|
67
|
+
# ==Calculation Support
|
68
|
+
# Now with +calculate+ support for +sum+, +min+, and +max+ for integer columns and +count+ for all columns
|
69
|
+
# Cache hits do not occur if options other than +distinct+ are used.
|
70
|
+
#
|
71
|
+
# Cache hits:
|
72
|
+
# Neighborhood.count
|
73
|
+
# Neighborhood.count :name, :distinct => true
|
74
|
+
# Neighborhood.sum :id
|
75
|
+
# Neighborhood.max :id
|
76
|
+
# Not cache hits:
|
77
|
+
# Neighborhood.max :name
|
78
|
+
# Neighborhood.count :zip_code, :conditions => ['name = ?', 'Seattle']
|
79
|
+
#
|
80
|
+
# ==Convenience lookup
|
81
|
+
# Similar to acts_as_enumeration model returns the record where the
|
82
|
+
# <tt>acts_as_static_record :key</tt> option matches +lookup+
|
83
|
+
# If no key is specified, the primary_id column is used
|
84
|
+
#
|
85
|
+
# class User < ActiveRecord::Base
|
86
|
+
# acts_as_static_record :key => :user_name
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# Then later we can reference the objects by the user_name
|
90
|
+
# User[:blythe]
|
91
|
+
# User['snowgiraffe']
|
92
|
+
#
|
93
|
+
# The key used will be the underscore version of the name. Punctuation marks
|
94
|
+
# are removed. The following are equivalent:
|
95
|
+
# User[:blythe_snowgiraffeawesome]
|
96
|
+
# User['blythe-SNOWGIRaffe AWESome']
|
97
|
+
#
|
98
|
+
# user = User.first
|
99
|
+
# User[user.user_name] == user
|
100
|
+
#
|
101
|
+
# === Developers
|
102
|
+
# * Blythe Dunham http://snowgiraffe.com
|
103
|
+
#
|
104
|
+
# === Homepage
|
105
|
+
# * Github Project: http://github.com/blythedunham/static_record_cache/tree/master
|
106
|
+
# * Install: <tt>script/plugin install git://github.com/blythedunham/static_record_cache.git</tt>
|
107
|
+
module ActsAsStaticRecord
|
108
|
+
def self.extended(base)
|
109
|
+
base.send :class_inheritable_hash, :acts_as_static_record_options
|
110
|
+
base.acts_as_static_record_options = {}
|
111
|
+
base.extend ClassMethods
|
112
|
+
end
|
113
|
+
|
114
|
+
module ClassMethods#:nodoc:
|
115
|
+
# =acts_as_static_record
|
116
|
+
# Permanently caches subclasses of ActiveRecord that contains data that changes rarely.
|
117
|
+
#
|
118
|
+
# Includes support for:
|
119
|
+
# * Find by Id, all, first (association lookups)
|
120
|
+
# * Cached caluculations: <tt>Neighborhood.count</tt>s, sum, max, min
|
121
|
+
# * Convenience lookups: <tt>Neighborhood[:seattle]</tt>
|
122
|
+
# * Additional support for column name lookups: <tt>Neighborhood.find_by_name 'Seattle'</tt>
|
123
|
+
#
|
124
|
+
# == Install
|
125
|
+
# script/plugin install git://github.com/blythedunham/static_record_cache.git
|
126
|
+
#
|
127
|
+
# == Usage
|
128
|
+
# class SomeMostlyStaticClass < ActiveRecord::Base
|
129
|
+
# acts_as_static_record
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# Any finds that do not contain additional conditions, joins, and other arguments
|
133
|
+
# become a cache call. One advantage over the query cache is that the static cache is searched
|
134
|
+
# eliminating the need for +ActiveRecord+ to generate SQL
|
135
|
+
#
|
136
|
+
# When a cache key is specified with option <tt>:key</tt>, additional
|
137
|
+
# finder methods for ids and fields such as +find_by_id+ and +find_by_name_and_mother+
|
138
|
+
# are overwritten to search the cache when no arguments (conditions) are specified.
|
139
|
+
# If the cache key is not a column, then a finder method will be defined.
|
140
|
+
# acts_as_static_record :key => :some_instance_method
|
141
|
+
# Will define <tt>find_by_some_instance_method(value)</tt>
|
142
|
+
#
|
143
|
+
# === Options
|
144
|
+
# * <tt>:key</tt> - a method or column of the instance used to specify a cache key. This should be unique.
|
145
|
+
# * <tt>:find</tt> an additional find scope to specify <tt>:conditions</tt>,<tt>:joins</tt>, <tt>:select</tt>, <tt>:joins</ff> etc
|
146
|
+
# * <tt>:find_by_attribute_support</tt> - set to true to add additional functionality for finders such as +find_by_id_and_name+ to use a cache search. This option is probably best for Rails 2.3
|
147
|
+
# * <tt>:lookup_key</tt> - access the record from the class by a key name like <tt>User[:snowgiraffe]</tt>. <tt>:lookup_key</tt> is the column on which do do the lookup.
|
148
|
+
#
|
149
|
+
# === Examples
|
150
|
+
# Caches on Id and telephone carrier name
|
151
|
+
# class TelephoneCarrier < ActiveRecord::Base
|
152
|
+
# acts_as_static_method :key => :name
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# Caches the WhiteList on phone_number_digits (in addition to ID)
|
156
|
+
# create_table :sms_white_list, :force => true do |t|
|
157
|
+
# t.column :phone_number_id, :integer, :null => false
|
158
|
+
# t.column :notes, :string, :length => 100, :default => nil
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# class SmsWhiteList < ActiveRecord::Base
|
162
|
+
# belongs_to :phone_number
|
163
|
+
#
|
164
|
+
# acts_as_static_record :key => :phone_number_digits,
|
165
|
+
# :find => :select => 'carriers.*, phone_number.number as phone_number_digits'
|
166
|
+
# :joins => 'inner join phone_numbers on phone_numbers.carrier_id = carriers.id'
|
167
|
+
#
|
168
|
+
# def phone_number_digits
|
169
|
+
# self['phone_number_digits']||self.phone_number.number
|
170
|
+
# end
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# Direct cache hits
|
174
|
+
# SmsWhiteList.find_by_phone_number_digits('12065551234')
|
175
|
+
# SmsWhiteList.find_by_id(5)
|
176
|
+
# SmsWhiteList.find :all
|
177
|
+
#
|
178
|
+
# Searched cache hits
|
179
|
+
# SmsWhiteList.find_by_notes('Some note')
|
180
|
+
#
|
181
|
+
# ==Calculation Support
|
182
|
+
# Now with +calculate+ support for +sum+, +min+, and +max+ for integer columns and +count+ for all columns
|
183
|
+
# Cache hits do not occur if options other than +distinct+ are used.
|
184
|
+
#
|
185
|
+
# Cache hits:
|
186
|
+
# Neighborhood.count
|
187
|
+
# Neighborhood.count :name, :distinct => true
|
188
|
+
# Neighborhood.sum :id
|
189
|
+
# Neighborhood.max :id
|
190
|
+
# Not cache hits:
|
191
|
+
# Neighborhood.max :name
|
192
|
+
# Neighborhood.count :zip_code, :conditions => ['name = ?', 'Seattle']
|
193
|
+
#
|
194
|
+
# ==Convenience lookup
|
195
|
+
# Similar to acts_as_enumeration model returns the record where the
|
196
|
+
# <tt>acts_as_static_record :key</tt> option matches +lookup+
|
197
|
+
# If no key is specified, the primary_id column is used
|
198
|
+
#
|
199
|
+
# class User < ActiveRecord::Base
|
200
|
+
# acts_as_static_record :key => :user_name
|
201
|
+
# end
|
202
|
+
#
|
203
|
+
# Then later we can reference the objects by the user_name
|
204
|
+
# User[:blythe]
|
205
|
+
# User['snowgiraffe']
|
206
|
+
#
|
207
|
+
# The key used will be the underscore version of the name. Punctuation marks
|
208
|
+
# are removed. The following are equivalent:
|
209
|
+
# User[:blythe_snowgiraffeawesome]
|
210
|
+
# User['blythe-SNOWGIRaffe AWESome']
|
211
|
+
#
|
212
|
+
# user = User.first
|
213
|
+
# User[user.user_name] == user
|
214
|
+
def acts_as_static_record(options={})
|
215
|
+
|
216
|
+
acts_as_static_record_options.update(options) if options
|
217
|
+
|
218
|
+
if acts_as_static_record_options[:find_by_attribute_support]
|
219
|
+
extend ActsAsStaticRecord::DefineFinderMethods
|
220
|
+
end
|
221
|
+
|
222
|
+
extend ActsAsStaticRecord::SingletonMethods
|
223
|
+
include ActsAsStaticRecord::InstanceMethods
|
224
|
+
|
225
|
+
unless respond_to?(:find_without_static_record)
|
226
|
+
klass = class << self; self; end
|
227
|
+
klass.class_eval "alias_method_chain :find, :static_record"
|
228
|
+
klass.class_eval "alias_method_chain :calculate, :static_record"
|
229
|
+
end
|
230
|
+
|
231
|
+
define_static_cache_key_finder
|
232
|
+
|
233
|
+
class_eval do
|
234
|
+
before_save {|record| record.class.clear_static_record_cache }
|
235
|
+
before_destroy {|record| record.class.clear_static_record_cache }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
protected
|
240
|
+
# Define a method find_by_KEY if the specified cache key
|
241
|
+
# is not an active record column
|
242
|
+
def define_static_cache_key_finder#:nodoc:
|
243
|
+
return unless acts_as_static_record_options[:find_by_attribute_support]
|
244
|
+
#define the key column if it is not a hash column
|
245
|
+
if ((key_column = acts_as_static_record_options[:key]) &&
|
246
|
+
(!column_methods_hash.include?(key_column.to_sym)))
|
247
|
+
class_eval %{
|
248
|
+
def self.find_by_#{key_column}(arg)
|
249
|
+
self.static_record_cache[:key][arg.to_s]
|
250
|
+
end
|
251
|
+
}, __FILE__, __LINE__
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
module InstanceMethods
|
257
|
+
|
258
|
+
# returns the lookup key for this record
|
259
|
+
# For example if the defintion in +User+ was
|
260
|
+
# acts_as_static_record :key => :user_name
|
261
|
+
#
|
262
|
+
# user.user_name
|
263
|
+
# => "Blythe Snow Giraffe"
|
264
|
+
#
|
265
|
+
# user.static_record_lookup_key
|
266
|
+
# => 'blythe_snow_giraffe'
|
267
|
+
#
|
268
|
+
# which could then be used to access the record like
|
269
|
+
# User['snowgiraffe']
|
270
|
+
# => <User id: 15, user_name: "Blythe Snow Giraffe">
|
271
|
+
#
|
272
|
+
def static_record_lookup_key
|
273
|
+
self.class.static_record_lookup_key(self)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
module SingletonMethods
|
278
|
+
|
279
|
+
# Similar to acts_as_enumeration model returns the record where the
|
280
|
+
# <tt>acts_as_static_record :key</tt> option matches +lookup+
|
281
|
+
# If no key is specified, the primary_id column is used
|
282
|
+
#
|
283
|
+
# class User < ActiveRecord::Base
|
284
|
+
# acts_as_static_record :key => :user_name
|
285
|
+
# end
|
286
|
+
# Then later we can reference the objects by the user_name
|
287
|
+
# User[:blythe]
|
288
|
+
# User['snowgiraffe']
|
289
|
+
#
|
290
|
+
# The key used will be the underscore version of the name. Punctuation marks
|
291
|
+
# are removed. The following are equivalent:
|
292
|
+
# User[:blythe_snowgiraffeawesome]
|
293
|
+
# User['blythe-SNOWGIRaffe AWESome']
|
294
|
+
#
|
295
|
+
# user = User.first
|
296
|
+
# User[user.user_name] == user
|
297
|
+
def [](lookup_name)
|
298
|
+
(static_record_cache[:lookup]||= begin
|
299
|
+
|
300
|
+
static_record_cache[:primary_key].inject({}) do |lookup, (k,v)|
|
301
|
+
|
302
|
+
lookup[v.static_record_lookup_key] = v
|
303
|
+
lookup
|
304
|
+
|
305
|
+
end
|
306
|
+
end)[static_record_lookup_key(lookup_name)]
|
307
|
+
|
308
|
+
end
|
309
|
+
|
310
|
+
# Parse the lookup key
|
311
|
+
def static_record_lookup_key(value)#:nodoc:
|
312
|
+
if value.is_a? self
|
313
|
+
static_record_lookup_key(
|
314
|
+
value.send(
|
315
|
+
acts_as_static_record_options[:lookup_key] ||
|
316
|
+
acts_as_static_record_options[:key] ||
|
317
|
+
primary_key
|
318
|
+
)
|
319
|
+
)
|
320
|
+
else
|
321
|
+
value.to_s.gsub(' ', '_').gsub(/\W/, "").underscore
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Search the cache for records with the specified attributes
|
326
|
+
#
|
327
|
+
# * +finder+ - Same as with +find+ specify <tt>:all</tt>, <tt>:last</tt> or <tt>:first</tt>
|
328
|
+
# <tt>:all</all> returns an array of active records, <tt>:last</tt> or <tt>:first</tt> returns a single instance
|
329
|
+
# * +attributes+ - a hash map of fields (or methods) => values
|
330
|
+
#
|
331
|
+
# User.find_in_static_cache(:first, {:password => 'fun', :user_name => 'giraffe'})
|
332
|
+
def find_in_static_record_cache(finder, attributes)#:nodoc:
|
333
|
+
list = static_record_cache[:primary_key].values.inject([]) do |list, record|
|
334
|
+
unless attributes.select{|k,v| record.send(k).to_s != v.to_s}.any?
|
335
|
+
return record if finder == :first
|
336
|
+
list << record
|
337
|
+
end
|
338
|
+
list
|
339
|
+
end
|
340
|
+
finder == :all ? list : list.last
|
341
|
+
end
|
342
|
+
|
343
|
+
# Perform find by searching through the static record cache
|
344
|
+
# if only an id is specified
|
345
|
+
def find_with_static_record(*args)#:nodoc:
|
346
|
+
if scope(:find).nil? && args
|
347
|
+
if args.first.is_a?(Fixnum) &&
|
348
|
+
((args.length == 1 ||
|
349
|
+
(args[1].is_a?(Hash) && args[1].values.delete(nil).nil?)))
|
350
|
+
return static_record_cache[:primary_key][args.first]
|
351
|
+
elsif args.first == :all && args.length == 1
|
352
|
+
return static_record_cache[:primary_key].values
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
find_without_static_record(*args)
|
357
|
+
end
|
358
|
+
|
359
|
+
# Override calculate to compute data in memory if possible
|
360
|
+
# Only processes results if there is no scope and the no keys other than distinct
|
361
|
+
def calculate_with_static_record(operation, column_name, options={})#:nodoc:
|
362
|
+
if scope(:find).nil? && !options.any?{ |k,v| k.to_s.downcase != 'distinct' }
|
363
|
+
key = "#{operation}_#{column_name}_#{options.none?{|k,v| v.blank? }}"
|
364
|
+
static_record_cache[:calc][key]||=
|
365
|
+
case operation.to_s
|
366
|
+
when 'count' then
|
367
|
+
#count the cache if we want all or the unique primary key
|
368
|
+
if ['all', '', '*', primary_key].include?(column_name.to_s)
|
369
|
+
static_record_cache[:primary_key].length
|
370
|
+
|
371
|
+
#otherwise compute the length of the output
|
372
|
+
else
|
373
|
+
static_records_for_calculation(column_name, options) {|records| records.length }
|
374
|
+
end
|
375
|
+
#compute the method directly on the result array
|
376
|
+
when 'sum', 'max', 'min' then
|
377
|
+
|
378
|
+
if columns_hash[column_name.to_s].try(:type) == :integer
|
379
|
+
static_records_for_calculation(column_name, options) {|records| records.send(operation).to_i }
|
380
|
+
end
|
381
|
+
|
382
|
+
end
|
383
|
+
|
384
|
+
return static_record_cache[:calc][key] if static_record_cache[:calc][key]
|
385
|
+
|
386
|
+
end
|
387
|
+
calculate_without_static_record(operation, column_name, options)
|
388
|
+
end
|
389
|
+
|
390
|
+
# Return the array of results to calculate if they are available
|
391
|
+
def static_records_for_calculation(column_name, options={}, &block)#:nodoc:
|
392
|
+
if columns_hash.has_key?(column_name.to_s)
|
393
|
+
records = static_record_cache[:primary_key].values.collect(&(column_name.to_sym)).compact
|
394
|
+
results = (options[:distinct]||options['distinct']) ? records.uniq : records
|
395
|
+
block ? yield(results) : results
|
396
|
+
else
|
397
|
+
nil
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Clear (and reload) the record cache
|
402
|
+
def clear_static_record_cache
|
403
|
+
@static_record_cache = nil
|
404
|
+
end
|
405
|
+
|
406
|
+
# The static record cache
|
407
|
+
def static_record_cache
|
408
|
+
@static_record_cache||= initialize_static_record_cache
|
409
|
+
end
|
410
|
+
|
411
|
+
protected
|
412
|
+
|
413
|
+
# Find all the record and initialize the cache
|
414
|
+
def initialize_static_record_cache#:nodoc:
|
415
|
+
return unless @static_record_cache.nil?
|
416
|
+
records = self.find_without_static_record(:all, acts_as_static_record_options[:find]||{})
|
417
|
+
@static_record_cache = records.inject({:primary_key => {}, :key => {}, :calc => {}}) do |cache, record|
|
418
|
+
cache[:primary_key][record.send(self.primary_key)] = record
|
419
|
+
if acts_as_static_record_options[:key]
|
420
|
+
cache[:key][record.send(acts_as_static_record_options[:key])] = record
|
421
|
+
end
|
422
|
+
cache
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
|
428
|
+
# This module is designed to define finder methods such as find_by_id to
|
429
|
+
# search through the cache if no additional arguments are specified
|
430
|
+
# The likelyhood of this working with < Rails 2.3 is pretty low.
|
431
|
+
module DefineFinderMethods#:nodoc:
|
432
|
+
|
433
|
+
#alias chain the finder method to the static_rc method
|
434
|
+
#base_method_id would be like find_by_name
|
435
|
+
def define_static_rc_alias(base_method_id)#:nodoc:
|
436
|
+
if !respond_to?("#{base_method_id}_without_static_rc") &&
|
437
|
+
respond_to?(base_method_id) && respond_to?("#{base_method_id}_with_static_rc")
|
438
|
+
|
439
|
+
klass = class << self; self; end
|
440
|
+
klass.class_eval "alias_method_chain :#{base_method_id}, :static_rc"
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Retrieve the method name to call based on the attributes
|
445
|
+
# Single attributes on primary key or the specified key call directly to the cache
|
446
|
+
# All other methods iterate through the cache
|
447
|
+
def static_record_finder_method_name(finder, attributes)#:nodoc:
|
448
|
+
method_to_call = "find_in_static_record_cache(#{finder.inspect}, #{attributes.inspect})"
|
449
|
+
if attributes.length == 1
|
450
|
+
key_value = case attributes.keys.first.to_s
|
451
|
+
when self.primary_key then [:primary_key, attributes.values.first.to_i]
|
452
|
+
when acts_as_static_record_options[:key] then [:key, attributes.values.first.to_s]
|
453
|
+
end
|
454
|
+
|
455
|
+
method_to_call = "static_record_cache[#{key_value[0].inspect}][#{key_value[1].inspect}]" if key_value
|
456
|
+
end
|
457
|
+
method_to_call
|
458
|
+
end
|
459
|
+
|
460
|
+
# Define the finder method on the class, and return the name of the method
|
461
|
+
# Ex. find_by_id will define find_by_id_with_static_rc
|
462
|
+
#
|
463
|
+
# The cache is searched if no additional arguments (:conditions, :joins, etc) are specified
|
464
|
+
# If additional arguments do exist find_by_id_without_static_rc is invoked
|
465
|
+
def define_static_record_finder_method(method_id, finder, bang, attributes)#:nodoc:
|
466
|
+
method_to_call = static_record_finder_method_name(finder, attributes)
|
467
|
+
method_with_static_record = "#{method_id}_with_static_rc"
|
468
|
+
|
469
|
+
#override the method to search memory if there are no args
|
470
|
+
class_eval %{
|
471
|
+
def self.#{method_with_static_record}(*args)
|
472
|
+
if (args.dup.extract_options!).any?
|
473
|
+
#{method_id}_without_static_rc(*args)
|
474
|
+
else
|
475
|
+
result = #{method_to_call}
|
476
|
+
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
477
|
+
end
|
478
|
+
end
|
479
|
+
}, __FILE__, __LINE__
|
480
|
+
|
481
|
+
method_with_static_record
|
482
|
+
end
|
483
|
+
|
484
|
+
#Method missing is overridden to use cache calls for finder methods
|
485
|
+
def method_missing(method_id, *arguments, &block)#:nodoc:
|
486
|
+
|
487
|
+
# If the missing method is XXX_without_static_rc, define XXX
|
488
|
+
# using the superclass ActiveRecord::Base method_missing then
|
489
|
+
# Finally, alias chain it to XXX_with_static_rc
|
490
|
+
if ((match = method_id.to_s.match(/(.*)_without_static_rc$/)) &&
|
491
|
+
(base_method_id = match[1]))
|
492
|
+
begin
|
493
|
+
return super(base_method_id, *arguments, &block)
|
494
|
+
ensure
|
495
|
+
define_static_rc_alias(base_method_id)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
# If the missing method is a finder like find_by_name
|
500
|
+
# Define on the class then invoke find_by_name_with_static_rc
|
501
|
+
if match = ActiveRecord::DynamicFinderMatch.match(method_id)
|
502
|
+
attribute_names = match.attribute_names
|
503
|
+
if all_attributes_exists?(attribute_names) && match.finder?
|
504
|
+
attributes = construct_attributes_from_arguments(attribute_names, arguments)
|
505
|
+
method_name = define_static_record_finder_method(method_id, match.finder, match.bang?, attributes)
|
506
|
+
return self.send method_name, *arguments
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
#If nothing matches, invoke the super
|
511
|
+
super(method_id, *arguments, &block)
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
ActiveRecord::Base.extend ActsAsStaticRecord unless defined?(ActiveRecord::Base.acts_as_static_record_options)
|
517
|
+
|