require 'object_proxy' module ActiveRecord class CachedValue < ObjectProxy def initialize(owner, reflection) @owner, @reflection = owner, reflection reset end def reset @target = nil @loaded = false end def load reset load_target end def reload @owner.instance_variable_set("@#{@reflection.name}", nil) reset @target = find_target(true) @owner.send @reflection.name end alias update reload def clear clear_cache @owner.instance_variable_set("@#{@reflection.name}", nil) end def loaded? @loaded end def loaded @loaded = true end def target @target end protected def load_target return nil unless defined?(@loaded) @target = find_target unless loaded? @loaded = true @target end def find_target(skip_cache = false) target = find_target_from_cache unless skip_cache unless target target ||= @reflection.options[:sql] ? find_target_by_sql : find_target_by_eval update_cache(target) end target end def find_target_from_cache @owner.send(:read_attribute, cache_column) if has_cache? end def find_target_by_sql sql = sanitize_sql(interpolate_sql(@reflection.options[:sql])) result = @owner.class.connection.select_value(sql) result = typecast_result(result) result end def find_target_by_eval if @reflection.options[:eval].is_a?(String) eval(@reflection.options[:eval], @owner.send(:binding)) elsif @reflection.options[:eval].is_a?(Proc) @reflection.options[:eval].call(@owner) elsif @reflection.options[:eval].is_a?(Symbol) @owner.send @reflection.options[:eval] else raise ArgumentError.new("The :eval option on a cached_values must be a String or a Proc or a Symbol") end end def cache_column @reflection.options[:cache] end def has_cache? @reflection.options[:cache] && @owner.attribute_names.include?(@reflection.options[:cache].to_s) end def clear_cache update_cache(nil) end def update_cache(value) return unless has_cache? unless @owner.new_record? @owner.class.update_all(["#{cache_column} = ?", value], ["id = ?", @owner.id]) end @owner.send(:write_attribute, cache_column, value) end def typecast_result(result) if has_cache? typecast_sql_result_by_cache_column_type(result) else if result =~ /^\d+\.\d+$/ result.to_f elsif result =~ /^\d+$/ result.to_i else result end end end def typecast_sql_result_by_cache_column_type(result) type = @owner.column_for_attribute(cache_column).type case type when :integer result.to_i when :float result.to_f when :boolean [0,'0', 'NULL', 'nil', 'false', '', 'FALSE', 'False'].include?(result) else result end end def interpolate_sql(sql, record = nil) @owner.send(:interpolate_sql, sql, record) end def sanitize_sql(sql) @owner.class.send(:sanitize_sql, sql) end end end