merged cont.
[opensuse:yast-rest-service.git] / webyast / vendor / plugins / static_record_cache / lib / acts_as_static_record.rb
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