Question

J'ai une arborescence d'objets d'enregistrement actifs, quelque chose comme:

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

Il est trop coûteux de recalculer le calcul compliqué à chaque fois. Donc, j'ai besoin d'un moyen de mettre en cache la valeur. Toutefois, si une partie est modifiée, il doit invalider son cache et celui de ses parents, grands parents, etc.

.

Comme brouillon, j'ai créé une colonne destinée à contenir le calcul mis en cache dans les "pièces". table, mais ça sent un peu pourri. Il semble qu'il devrait exister un moyen plus propre de mettre en cache les valeurs calculées sans les insérer dans le "vrai" colonnes.

Était-ce utile?

La solution

  1. Vous pouvez farcir les valeurs actuellement mises en cache dans le cache Rails (utilisez memcached si vous souhaitez qu'il soit distribué).

  2. Le problème, c’est l’expiration du cache, mais l’expiration du cache est peu commune, non? Dans ce cas, nous pouvons simplement parcourir chacun des objets parents et zapper son cache également. J'ai ajouté un peu de magie ActiveRecord à votre classe pour simplifier l'accès aux objets parents. Vous n'avez même pas besoin de toucher à votre base de données. N'oubliez pas d'appeler Part.sweep_complicated_cache (some_part) selon le cas dans votre code - vous pouvez l'insérer dans les rappels, etc., mais je ne peux pas l'ajouter pour vous car je ne comprends pas quand complex_calculation est en train de changer.

    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
    

Autres conseils

Je suggère d'utiliser des rappels d'association.

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

Sympa et facile =)

Avoir un champ similaire à un cache de compteur. Par exemple: order_items_amount et que ce soit un champ calculé mis en cache.

Utilisez un filtre after_save pour recalculer le champ sur tout ce qui peut modifier cette valeur. (Y compris le disque lui-même)

Edit: C’est essentiellement ce que vous avez maintenant. Je ne connais aucune solution plus propre à moins que vous ne souhaitiez stocker les champs calculés mis en cache dans une autre table.

Utiliser un before_save ou un ActiveRecord Observer est la meilleure solution pour vérifier que la valeur mise en cache est à jour. J'utiliserais un before_save, puis je vérifierais si la valeur que vous utilisez dans le calcul a réellement changé. De cette façon, vous n'avez pas besoin de mettre à jour le cache si vous n'en avez pas besoin.
Stocker la valeur dans la base de données vous permettra de mettre en cache les calculs sur plusieurs requêtes. Une autre option consiste à stocker la valeur dans memcache. Vous pouvez créer un accesseur et un paramètre spéciaux pour cette valeur, qui peuvent vérifier le memcache et le mettre à jour si nécessaire.
Autre pensée: y aura-t-il des cas où vous modifierez une valeur dans l'un des modèles et nécessitez une mise à jour du calcul avant de sauvegarder? Dans ce cas, vous devrez nettoyer la valeur du cache chaque fois que vous mettez à jour une des valeurs de calcul dans le modèle, et non avec before_save.

J'ai constaté qu'il y avait parfois de bonnes raisons de dénormaliser les informations de votre base de données. Je travaille sur quelque chose de similaire dans une application et je ne fais que recalculer ce champ chaque fois que la collection change.

Il n’utilise pas de cache et stocke les données les plus récentes dans la base de données.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top