2008-10-08 24 views
13

que tienen un árbol de objetos de registro activos, algo así como:¿Cómo puedo almacenar en caché una columna calculada en rieles?

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 

Es demasiado costoso para volver a calcular la complicated_calculation cada vez. Entonces, necesito una forma de almacenar el valor en caché. Sin embargo, si se cambia alguna parte, debe invalidar su caché y la memoria caché de su principal y abuelos, etc.

Como borrador preliminar, creé una columna para mantener el cálculo en caché en la tabla "partes" , pero esto huele un poco podrido. Parece que debe haber una manera más limpia de almacenar en caché los valores calculados sin rellenarlos junto a las columnas "reales".

Respuesta

6
  1. usted puede rellenar los valores efectivamente en caché en la memoria caché de carriles (uso memcached si requiere que se distribuya).

  2. El bit duro es el vencimiento de la caché, pero el vencimiento de la caché es poco común, ¿no? En ese caso, podemos pasar por encima de cada uno de los objetos principales a su vez y también borrar su caché. Agregué un poco de magia de ActiveRecord a tu clase para hacer que la simplicidad de los objetos padres sea más sencilla, y ni siquiera necesitas tocar tu base de datos. Recuerde llamar al Part.sweep_complicated_cache(some_part) según corresponda en su código: puede poner esto en devoluciones de llamada, etc., pero no puedo agregarlo porque no entiendo cuando el complicated_calculation está cambiando.

    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

Tienen un campo similar a un contador de caché. Por ejemplo: order_items_amount y tiene que ser un campo calculado en caché.

Utilice un filtro after_save para recalcular el campo en cualquier cosa que pueda modificar ese valor. (Incluyendo el registro en sí)

Editar: Esto es básicamente lo que tiene ahora. No sé de ninguna solución más limpia a menos que desee almacenar los campos calculados en caché en otra tabla.

2

El uso de un before_save o un ActiveRecord Observer es el camino a seguir para asegurarse de que el valor en caché esté actualizado. Usaría before_save y luego verificaría si el valor que usas en el cálculo realmente cambió. De esta forma, no tiene que actualizar la memoria caché si no es necesario.
Almacenar el valor en el archivo db le permitirá almacenar en caché los cálculos sobre múltiples solicitudes. Otra opción para esto es almacenar el valor en Memcache. Puede crear un acceso y un setter especial para ese valor que pueda verificar el Memcache y actualizarlo si es necesario.
Otra idea: ¿Habrá casos en los que cambie un valor en uno de los modelos y necesite que se actualice el cálculo antes de realizar el guardado? En ese caso, deberá ensuciar el valor de caché siempre que actualice alguno de los valores de cálculo en el modelo, no con un before_save.

26

I sugerir el uso de devoluciones de llamada de asociación.

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 

agradable y fácil =)

+1

Supongo que esto no manejaría el caso en el que creas una sub_parte desde la otra dirección (* no * a través de has_many), como esto: Part.create (: parent_part => the_parent_part). Probablemente agregue una devolución de llamada after_create en la Parte solo para asegurarme de que count_sub_parts se active en ese caso también ... –

+1

Acabo de hacer una pequeña prueba en Rails 4 y verificada, estos hooks after_add y after_remove no se activan cuando se crea el registro secundario (sub_part) directamente –

1

me he dado cuenta que a veces hay buenas razones para de-normalizar la información en su base de datos. Tengo algo similar en una aplicación en la que estoy trabajando y simplemente vuelvo a calcular ese campo cada vez que cambia la colección.

No utiliza un caché y almacena la cifra más actualizada en la base de datos.

Cuestiones relacionadas