Pregunta

Tengo un árbol de objetos de registro activo, 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

Es demasiado costoso volver a calcular el valor de la función de cálculo complicado. Por lo tanto, necesito una forma de almacenar el valor en caché. Sin embargo, si se modifica alguna parte, debe invalidar su caché y el caché de su padre y su abuelo, etc.

Como borrador, creé una columna para guardar el cálculo en caché en las partes " " Mesa, pero esto huele un poco podrido. Parece que debería haber una forma más limpia de almacenar en caché los valores calculados sin rellenarlos al lado del " real " columnas.

¿Fue útil?

Solución

  1. Puede rellenar los valores realmente almacenados en caché en el caché de Rails (use memcached si necesita que se distribuya).

  2. El bit duro es la caducidad de la caché, pero la caducidad de la caché es poco común, ¿verdad? En ese caso, podemos hacer un bucle sobre cada uno de los objetos principales y, a su vez, utilizar su caché. Agregué algo de magia ActiveRecord a su clase para hacer que los objetos principales sean sencillos, y ni siquiera necesita tocar su base de datos. Recuerde llamar a Part.sweep_complicated_cache (some_part) según corresponda en su código; puede poner esto en las devoluciones de llamada, etc., pero no puedo agregarlo porque no entiendo cuándo 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
    

Otros consejos

Sugiero utilizar 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 =)

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

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

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

El uso de un before_save o un ActiveRecord Observer es la manera de asegurarse de que el valor en caché esté actualizado. Usaría un before_save y luego verificaría si el valor que usó en el cálculo realmente cambió. De esa manera, no tiene que actualizar el caché si no necesita hacerlo.
Almacenar el valor en la base de datos le permitirá almacenar en caché los cálculos sobre varias solicitudes. Otra opción para esto es almacenar el valor en memcache. Puede hacer un descriptor de acceso y configuración especial para ese valor que puede verificar el memcache y actualizarlo si es necesario.
Otro pensamiento: ¿Habrá casos en los que cambiará un valor en uno de los modelos y necesitará que el cálculo se actualice antes de guardar? En ese caso, tendrá que ensuciar el valor del caché cada vez que actualice cualquiera de los valores de cálculo en el modelo, no con un before_save.

Descubrí que a veces hay buenas razones para des-normalizar la información en su base de datos. Tengo algo similar en una aplicación en la que estoy trabajando y simplemente recalculo ese campo cada vez que cambia la colección.

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top