static_record_cache 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
+