Как кешировать вычисляемый столбец в рельсах?

У меня есть дерево активных объектов записи, например:

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

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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
16
0
8 991
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

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

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

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

Ответ принят как подходящий
  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
    

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

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

Красиво и просто =)

Я предполагаю, что это не справится со случаем, когда вы создаете sub_part с другого направления (нет через has_many), например: Part.create (: parent_part => the_parent_part). Я бы, вероятно, добавил обратный вызов after_create в Part, чтобы убедиться, что count_sub_parts также запускается в этом случае ...

Tyler Rick 29.11.2012 23:35

Просто провел небольшой тест в Rails 4 и проверил, эти хуки after_add и after_remove не срабатывают, когда вы создаете дочернюю запись (sub_part) напрямую

Nathan Wallace 09.09.2013 00:45

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

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

Другие вопросы по теме