У меня есть дерево активных объектов записи, например:
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 слишком дорого. Итак, мне нужен способ кэшировать значение. Однако, если какая-либо часть изменяется, ей необходимо сделать недействительными свой кеш и кеш своего родителя, дедушки и бабушки и т. д.
В качестве черновика я создал столбец для хранения кэшированных вычислений в таблице «частей», но это немного гнилое. Похоже, должен быть более чистый способ кэшировать вычисленные значения, не забивая их рядом с «настоящими» столбцами.





Имейте поле, подобное кешу счетчика. Например: order_items_amount и пусть это будет кэшируемое вычисляемое поле.
Используйте фильтр after_save, чтобы пересчитать поле для всего, что может изменить это значение. (Включая саму запись)
Обновлено: это в основном то, что у вас есть сейчас. Я не знаю более чистого решения, если вы не хотите хранить кешированные вычисляемые поля в другой таблице.
Либо использование before_save, либо ActiveRecord Observer - это способ убедиться, что кешированное значение актуально. Я бы использовал before_save, а затем проверял, действительно ли изменилось значение, которое вы используете в расчетах. Таким образом, вам не нужно обновлять кеш, если вам это не нужно. Сохранение значения в базе данных позволит вам кэшировать вычисления по нескольким запросам. Другой вариант - сохранить значение в кэше памяти. Вы можете создать специальный метод доступа и установщик для этого значения, который может проверять кэш памяти и обновлять его при необходимости. Еще одна мысль: будут ли случаи, когда вы измените значение в одной из моделей и потребуется обновить расчет перед сохранением? В этом случае вам нужно будет загрязнять значение кеша всякий раз, когда вы обновляете какое-либо из расчетных значений в модели, а не с помощью before_save.
Вы можете поместить фактически кэшированные значения в кеш Rails (используйте memcached, если вы хотите, чтобы он был распределен).
Трудный момент - истечение срока действия кеша, но истечение срока действия кеша случается редко, верно? В этом случае мы можем просто перебрать каждый из родительских объектов по очереди и также заблокировать его кеш. Я добавил в ваш класс некоторую магию 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
Красиво и просто =)
Просто провел небольшой тест в Rails 4 и проверил, эти хуки after_add и after_remove не срабатывают, когда вы создаете дочернюю запись (sub_part) напрямую
Я обнаружил, что иногда есть веская причина для отмены нормализации информации в вашей базе данных. У меня есть нечто подобное в приложении, над которым я работаю, и я просто пересчитываю это поле каждый раз, когда коллекция изменяется.
Он не использует кеш и хранит самые свежие данные в базе данных.
Я предполагаю, что это не справится со случаем, когда вы создаете sub_part с другого направления (нет через has_many), например: Part.create (: parent_part => the_parent_part). Я бы, вероятно, добавил обратный вызов after_create в Part, чтобы убедиться, что count_sub_parts также запускается в этом случае ...