Загрузка...

Ruby & Rails: веб-разработка с удовольствием

Ruby on Rails — фреймворк для создания веб-приложений. Является открытым программным обеспечением (лицензия MIT). Здесь мы обсуждаем новости RoR, делимся учебными материалами и интересными находками С RoR даже сложные веб-приложения могут быть написаны за считанные дни. Это действительно разработка с удовольствием!
     

Кэширование значений методов в аттрибуте

01.05.09, 15:51
Автор artem.voroztsov

Иногда значение рейтинга объектов какой-либо модели зависит от множества факторов и вычисляется сложно и ресурсоёмко, и хочется с этим что-то сделать

1) можно оставить как есть (правда замучаетесь заниматься оптимизацией запросов, где нужно сортировать по рейтингу)

2) можно добавить атрибут, в котором кешируется значение рейтинга

Во втором случае придётся писать разнообразные after_create для моделей, которые могут повилять на значение рейтинга (если, конечно, нам хочется, чтобы значение рейтинга было актульным). Но в жизни часто не требуется абсолютной актуальности значений рейтингов. Допустима задержка в минуту или даже более (час, день, два), потому мы можем действовать более гибко.

Итак, не будет думать о всех тех случаях, которые потенциально могут првиести к обновлению рейтинга. Просто создадим ещё один аттрибут - rating_expire_at, который будем устанавливать на 1 минуту вперед после обновление его значения, а при использовании аттрибута rating проверять, не пора ли его обновить.

Можно не программировать эту идею самому, а воспользоваться следующим сниппетом с элементами (как же без этого :)) метапрограммирования (сложностью где-то в 15 кю).

This module allows you to cache value of a method in database as model attribute. Аttribute < NAME> can be accompanied with corresponding < NAME>_expire_at attribute of datetime type.
Usage:

  1) initialize: to any of you initializers add line

ActiveRecord::Base.send :include CacheAsAttribute 
   
  2) edit model:

class Network
  cache_as_attribute :member_count, :expire_in => 1.hour do |n|
    n.members.count
  end
end

 You can

  • override default name of attribute with expiration time
  • give method name caclulating fresh value instead of associated block
  • specify expire_in value

 Example:

class Network
  cache_as_attribute :member_count,
    :expire_in => 1.hour,
    :method => :member_count_eval,
    :expire_at_attr => :members_expire_at
 
  def members_count_eval
     network.members.count
  end
end

You can call network.members_count(true) to force update of the attribute.
If nor :expire_at_attr neither :expire_in option is specified then this attribute will be cached untill you call it with force=true.

You can write :expire_at_attr => true to use default name of attribute with expiration time. Default value of expite_in is 1.minute.

module CacheAsAttribute
  def self.included(klass)
    klass.class_eval do
      extend  ClassMethods
      include InstanceMethods
    end
  end
  module InstanceMethods
    def eval_attr_new_value(thing)
      case thing
      when Proc
        thing[self]
      when Symbol
        send(thing)
      when String
        eval thing
      else
        raise ArgumentError, 
          "Inappropriate class of thing to eval: '#{thing.class}'"
      end
    end
  end
  module ClassMethods
    def cache_as_attribute(attr_name, options={}, &block)
      expire_in = options[:expire_in]
      options[:expire_at_attr] ||= true if expire_in
      expire_at_attr_name = options[:expire_at_attr]
      feature =  :cache_as_attribute
      expire_in ||= 60 if expire_at_attr_name
      method = options[:method] || block
      if expire_at_attr_name == true
        expire_at_attr_name = "#{attr_name}_expire_at"
      end
      old_method = "#{attr_name}_without_#{feature}"
      define_method("#{attr_name}_with_#{feature}") do |*args|
        force = args.first
        if force or (
          expire_at_attr_name && 
          (!(t=self.send(expire_at_attr_name)) || t < Time.now)
        ) then
          self.send("#{attr_name}=", eval_attr_new_value(method))
          self.send("#{expire_at_attr_name}=", Time.now + expire_in)
          self.update_selected_attributes(attr_name, expire_at_attr_name)
        end
        self.send(old_method)
      end
      unless self.instance_methods.include?(attr_name)
        define_method(attr_name) do
          self[attr_name]
        end
      end
      alias_method_chain attr_name, feature 
    end
  end 
end

 В этом коде используется метод update_selected_attributes, который как то прижился у меня и активно используется. Он осуществляет сохранение избранные атрибутов объекта без выполнения валидаций и других фильтров. В аргументах он принимает либо список имен атрибутов, либо хэш {имя_атрибута=>значение}:

ActiveRecord::Base.class_eval do
  def update_selected_attributes(*attributes)
    attributes = attributes.first if attributes.first.is_a?(Enumerable) && attributes.size == 1  
    if attributes.is_a?(Hash)
      attributes.each do |k,v|
        self[k] = v
      end
      attributes = attributes.keys
    end 
    
    self.class.update_all(
      attributes.map{|a|    
        a = a.to_s
        if self.class.serialized_attributes[a]
          a + "='" + self[a].to_yaml.gsub("\n",'\n').gsub("'"){"\\'"} + "'"
        else
          self.class.send(:sanitize_sql_hash_for_assignment, a => self[a])
        end
      }.join(', '),
      {:id => self.id}
    )
  end
end

список файлов:
active_record_ext.rb
cache_as_attribute.rb

Комментарии

Войдите, чтобы оставить комментарий