Como posso armazenar uma coluna calculada em trilhos?
-
05-07-2019 - |
Pergunta
Eu tenho uma árvore de objetos do Active Record, algo como:
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
É muito caro para recalcular o complicated_calculation cada vez. Então, eu preciso de uma maneira de armazenar em cache o valor. No entanto, se alguma parte é alterada, ele precisa para invalidar o cache eo cache de seu pai e avô, etc.
Como um rascunho, eu criei uma coluna para conter o cálculo em cache na tabela de "partes", mas isto cheira um pouco podre. Parece que deve haver uma maneira mais limpa para armazenar em cache os valores calculados sem enchê-los ao lado das colunas "reais".
Solução
-
Você pode encher os valores realmente colocado na cache do Rails (uso memcached se você exigir que ele seja distribuído).
-
O pouco difícil é cache de expiração, mas o cache de expiração é incomum, certo? Nesse caso, podemos apenas loop sobre cada um dos objetos pai por sua vez, e zap seu cache também. Eu adicionei um pouco de magia ActiveRecord para sua classe para fazer chegar o pai objetos própria simplicidade - e você não precisa mesmo de tocar o seu banco de dados. Lembre-se de chamar
Part.sweep_complicated_cache(some_part)
como apropriado no seu código -. Você pode colocar isso em retornos de chamada, etc, mas não pode adicioná-lo para você, porque eu não entendo quandocomplicated_calculation
está mudandoclass 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
Outras dicas
Eu sugiro usar callbacks de associação.
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
agradável e fácil =)
Tenha um campo semelhante a um cache balcão. Por exemplo: order_items_amount e tem que ser um campo calculado em cache.
Use um filtro after_save para recalcular o campo em qualquer coisa que pode modificar esse valor. (Incluindo o registro em si)
Edit: Este é basicamente o que você tem agora. Eu não sei de qualquer solução de limpeza a menos que você queria para armazenar campos calculados em cache em outra tabela.
De qualquer usando um before_save ou um Observer ActiveRecord é o caminho a percorrer para garantir que o valor em cache é up-to-date. Gostaria de usar um before_save e, em seguida, verifique se o valor que você utilizar no cálculo realmente mudou. Dessa forma, você não tem que atualizar o cache se você não precisa.
Armazenar o valor no db lhe permitirá armazenar em cache os cálculos sobre várias solicitações. Outra opção para isso é para armazenar o valor em memcache. Você pode fazer um acessor especial e setter para esse valor que pode verificar o cache de memória e atualizá-lo se necessário.
Outro pensamento: Haverá casos em que você vai mudar um valor em um dos modelos e precisam do cálculo ser atualizado antes de fazer o salvamento? Nesse caso, você terá que suja o valor do cache sempre que você atualizar qualquer um dos valores de cálculo no modelo, não com um before_save.
Descobri que às vezes há uma boa razão para de-normalize informações em seu banco de dados. Eu tenho algo semelhante em um aplicativo que estou trabalhando e eu apenas re-calculam que campo a qualquer hora as mudanças de cobrança.
Ele não usa um cache e armazena os mais modernos figura da data no banco de dados.