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 ADDED
@@ -0,0 +1,2 @@
1
+ pkg
2
+ tmp
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
@@ -0,0 +1,2 @@
1
+
2
+ require File.join( File.dirname(__FILE__), 'lib', 'static_record_cache')
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
+