2008-10-08 5 views
13

У меня есть дерево активных записей объектов, что-то вроде:Как я могу кэшировать вычисляемый столбец в рельсах?

class Part < ActiveRecord::Base 
    has_many :sub_parts, :class_name => "Part" 

    def complicated_calculation 
    if sub_parts.size > 0 
     return self.sub_parts.inject(0){ |sum, current| sum + current.complicated_calculation } 
    else 
     sleep(1) 
     return rand(10000) 
    end 
    end 

end 

Это слишком дорого, чтобы пересчитать complicated_calculation каждый раз. Итак, мне нужен способ кэширования значения. Однако, если какая-либо часть изменена, ей необходимо аннулировать ее кеш и кеш ее родителя, а также дедушку и бабушку и т. Д.

Как черновик, я создал столбец для хранения кэшированных вычислений в таблице «детали» , но это немного пахнет. Похоже, что должен быть более чистый способ кэшировать рассчитанные значения, не набивая их вдоль «реальных» столбцов.

ответ

6
  1. Вы можете хранить фактические кешированные значения в кеш-памяти Rails (используйте memcached, если требуется, чтобы оно было распространено).

  2. Жесткий бит - это срок действия кеша, но срок действия кеша необычен, не так ли? В этом случае мы можем просто перевернуть каждый из родительских объектов по очереди и также заблокировать его кеш. Я добавил магию ActiveRecord к вашему классу, чтобы сделать простоту самих родительских объектов - и вам даже не нужно прикасаться к базе данных. Не забудьте позвонить по номеру Part.sweep_complicated_cache(some_part) в соответствующем коде - вы можете поместить это в обратные вызовы и т. Д., Но я не могу добавить его для вас, потому что я не понимаю, когда меняется complicated_calculation.

    class Part < ActiveRecord::Base 
        has_many :sub_parts, :class_name => "Part" 
        belongs_to :parent_part, :class_name => "Part", :foreign_key => :part_id 
    
        @@MAX_PART_NESTING = 25 #pick any sanity-saving value 
    
        def complicated_calculation (...) 
        if cache.contains? [id, :complicated_calculation] 
         cache[ [id, :complicated_calculation] ] 
        else 
         cache[ [id, :complicated_calculation] ] = complicated_calculation_helper (...) 
        end 
        end 
    
        def complicated_calculation_helper 
        #your implementation goes here 
        end 
    
        def Part.sweep_complicated_cache(start_part) 
        level = 1 # keep track to prevent infinite loop in event there is a cycle in parts 
        current_part = self 
    
        cache[ [current_part.id, :complicated_calculation] ].delete 
        while ((level <= 1 < @@MAX_PART_NESTING) && (current_part.parent_part)) { 
        current_part = current_part.parent_part) 
        cache[ [current_part.id, :complicated_calculation] ].delete 
        end 
        end 
    end 
    
2

Имейте поле, похожее на счетчик кеша. Например: order_items_amount и это будет кэшированное вычисленное поле.

Используйте фильтр after_save для пересчета поля на все, что может изменить это значение. (Включая саму запись)

Редактировать: Это в основном то, что у вас есть сейчас. Я не знаю ни одного более чистого решения, если вы не хотите хранить кэшированные вычисленные поля в другой таблице.

2

Либо использование before_save, либо ActiveRecord Observer - это способ, чтобы убедиться, что кешированное значение является актуальным. Я использовал бы before_save, а затем проверил, изменилось ли значение, которое вы используете в расчете. Таким образом, вам не нужно обновлять кеш, если вам это не нужно.
Сохранение значения в db позволит вам кэшировать вычисления по нескольким запросам. Другим вариантом для этого является сохранение значения в memcache. Вы можете сделать специальный аксессуар и setter для этого значения, которое может проверить memcache и обновить его, если это необходимо.
Другая мысль: будут ли случаи, когда вы измените значение в одной из моделей и вам нужно будет обновить расчет до того, как вы сделаете сохранение? В этом случае вам придется загрязнять значение кэша всякий раз, когда вы обновляете любое из значений расчета в модели, а не с помощью before_save.

26

Я предлагаю использовать ассоциативные функции обратного вызова.

class Part < ActiveRecord::Base 
    has_many :sub_parts, 
    :class_name => "Part", 
    :after_add => :count_sub_parts, 
    :after_remove => :count_sub_parts 

    private 

    def count_sub_parts 
    update_attribute(:sub_part_count, calculate_sub_part_count) 
    end 

    def calculate_sub_part_count 
    # perform the actual calculation here 
    end 
end 

Хороший и легкий =)

+1

Я предполагаю, что это не будет обрабатывать случай, когда вы создаете sub_part с другого направления (* not * через has_many), например: Part.create (: parent_part => the_parent_part). Я бы, вероятно, добавил обратный вызов after_create для части, чтобы убедиться, что в этом случае срабатывает функция count_sub_parts ... – 2012-11-29 19:35:05

1

Я обнаружил, что иногда есть веские основания для снятия нормализуют информацию в базе данных. У меня есть что-то подобное в приложении, над которым я работаю, и я просто пересчитываю это поле в любое время, когда изменяется коллекция.

Он не использует кеш и сохраняет самую последнюю цифру в базе данных.