module CachedValues # :nodoc: def self.included(base) base.extend(ClassMethods) end module ClassMethods # USAGE: # # a very simple case in which cached_values works just like the .count method on a has_many association: # # class Company < ActiveRecord::Base # caches_value :total_employees, :sql => 'select count(*) from employees where company_id = #{id}' # end # # a more sophisticated example: # # class User < ActiveRecord::Base # has_many :trinkets # has_many :sales, :through => :trinkets # caches_value :remaining_trinket_sales_allotted, :sql => '... very complicated sql here ...' # end # # user = User.find(:first) # user.remaining_trinket_sales_allotted # => 70 # Trinket.delete_all # <= any operation that would affect our value # user.remaining_trinket_sales_allotted # => 70 # user.remaining_trinket_sales_allotted.reload # => 113 # # You can also calculate the value in Ruby. This can be done by a string to be eval'ed or a Proc. Both are evaluated # in the context of the record instance. # # class User < ActiveRecord::Base # caches_value :expensive_calculation, :eval => "some_big_expensize_calculation(self.id)" # caches_value :other_expensive_process, :eval => Proc.new {|record| record.other_expensize_process } # end # def caches_value(name, options = {}) reflection = create_cached_value_reflection(name, options) configure_dependency_for_cached_value(reflection) reflection.options[:cache] ||= reflection.name unless false == options[:cache] cached_value_accessor_method(reflection, ActiveRecord::CachedValue) cached_value_callback_methods(reflection) end private def configure_dependency_for_cached_value(reflection) if !reflection.options[:sql] && !reflection.options[:eval] raise ArgumentError, "You must specify either the :eval or :sql options for caches_value in #{self.name}" end if reflection.options[:sql] && reflection.options[:eval] raise ArgumentError, ":eval and :sql are mutually exclusive options. You may specify one or the other for caches_value in #{self.name}" end end def create_cached_value_reflection(name, options) options.assert_valid_keys(:sql, :eval, :cache, :clear, :load, :reload) reflection = ActiveRecord::Reflection::MacroReflection.new(:cached_value, name, options, self) write_inheritable_hash :reflections, name => reflection reflection end def cached_value_accessor_method(reflection, association_proxy_class) define_method(reflection.name) do |*params| force_reload = params.first unless params.empty? association = instance_variable_get("@#{reflection.name}") if association.nil? || force_reload association = association_proxy_class.new(self, reflection) instance_variable_set("@#{reflection.name}", association) force_reload ? association.reload : association.load end association.target.nil? ? nil : association end end def cached_value_callback_methods(reflection) %w{clear reload}.each do |operation| if events = reflection.options[operation.to_sym] events = [events] unless events.is_a?(Array) events.map! { |event| event.to_s } ActiveRecord::Callbacks::CALLBACKS.each do |callback| if events.include?(callback) send(callback, Proc.new {|record| record.send(reflection.name).send(operation)}) end end end end end end end