2012-08-23 3 views
9

Ho scritto un semplice modulo Cachebile che semplifica la cache dei campi aggregati in un modello principale. Il modulo richiede che l'oggetto padre implementa il metodo cacheable e un metodo calc_ per ogni campo che richiede la memorizzazione nella cache a livello padre.Come utilizzare la metaprogrammazione di Ruby per aggiungere callback a un modello Rails?

module Cacheable 
    def cache!(fields, *objects) 
    objects.each do |object| 
     if object.cacheable? 
     calc(fields, objects) 
     save!(objects) 
     end 
    end 
    end 

    def calc(fields, objects) 
    fields.each { |field| objects.each(&:"calc_#{field}") } 
    end 

    def save!(objects) 
    objects.each(&:save!) 
    end 
end 

Vorrei aggiungere i callback al modello ActiveRecord in cui è incluso questo modulo. Questo metodo richiede che il modello implementa un hash di modelli principali e nomi di campi che richiedono la memorizzazione nella cache.

def cachebacks(klass, parents) 
    [:after_save, :after_destroy].each do |callback| 
    self.send(callback, proc { cache!(CACHEABLE[klass], self.send(parents)) }) 
    end 
end 

Questo approccio funziona grande se aggiungo manualmente sia utilizzando callback come ad esempio:

after_save proc { cache!(CACHEABLE[Quote], *quotes.all) } 
after_destroy proc { cache!(CACHEABLE[Quote], *quotes.all) } 

Ma, sto ricevendo il seguente errore quando si tenta di utilizzare il metodo cachebacks per aggiungere questi per callback .

cachebacks(Quote, "*quotes.all") 

NoMethodError: undefined method `cachebacks' for #<Class:0x007fe7be3f2ae8> 

Come si aggiungono questi callback alla classe in modo dinamico?

+0

Scusa, non ho potuto dare un senso all'ultima parte. 'Quote' è un modello correlato? Potresti per favore pubblicare come appare la tua classe in questo momento? –

+0

La risposta che ho elaborato sulla base del tuo suggerimento dovrebbe spiegare le cose. Sono anche interessato all'approccio ActiveSupport :: Concern. – barelyknown

risposta

6

Questo sembra un buon caso per ActiveSupport::Concern. È possibile modificare il metodo di cachebacks po 'per aggiungerlo come un metodo di classe sulla classe tra cui:

module Cacheable 
    extend ActiveSupport::Concern 

    module ClassMethods 
    def cachebacks(&block) 
     klass = self 
     [:after_save, :after_destroy].each do |callback| 
     self.send(callback, proc { cache!(CACHEABLE[klass], *klass.instance_eval(&block)) }) 
     end 
    end 
    end 

    def cache!(fields, *objects) 
    # ... 
    end 

    # ... 
end 

usarlo:

class Example < ActiveRecord::Base 
    include Cacheable 
    cachebacks { all } 
end 

Il blocco si passa a cachebacks verrà eseguito nel contesto di la classe che lo chiama. In questo esempio, { all } equivale a chiamare Example.all e passare i risultati nel metodo cache!.


per rispondere alla tua domanda nei commenti, Concern incapsula un modello comune e stabilisce una convenzione in Rails. La sintassi è leggermente più elegante:

included do 
    # behaviors 
end 

# instead of 

def self.included(base) 
    base.class_eval do 
    # behaviors 
    end 
end 

Si sfrutta anche un'altra convenzione per includere automaticamente e correttamente metodi di classe e istanza. Se lo spazio dei nomi è in questi moduli nei moduli ClassMethods e InstanceMethods (anche se come hai visto, InstanceMethods è facoltativo), hai finito.

Infine, gestisce le dipendenze del modulo. La documentazione ne fornisce un buon esempio, ma in sostanza impedisce alla classe inclusa di includere esplicitamente moduli dipendenti oltre al modulo a cui è realmente interessato.

+1

Questa è una soluzione di bell'aspetto. Non ho mai utilizzato ActiveSupport :: Preoccupations. Quali sono i principali vantaggi rispetto a un approccio come quello che ho postato di seguito (il modulo Cacheable)? – barelyknown

+0

Ho aggiornato la mia risposta con ulteriori dettagli su "Concern". – Brandan

0

Come ho detto nel commento, potrei non avere ragione se non ho capito la tua domanda. Funzionerebbe per te?

module Cacheable 
    def self.included(base) 
    base.class_eval do 
     def self.cachebacks(klass, parents) 
     [:after_save, :after_destroy].each do |callback| 
      self.send(callback, proc { cache!(CACHEABLE[klass], self.send(parents)) }) 
     end 
     end 
    end 
    end 

    def cache!(fields, *objects) 
    objects.each do |object| 
     if object.cacheable? 
     calc(fields, objects) 
     save!(objects) 
     end 
    end 
    end 

    def calc(fields, objects) 
    fields.each { |field| objects.each(&:"calc_#{field}") } 
    end 

    def save!(objects) 
    objects.each(&:save!) 
    end 
end 
+0

Anche se non era esattamente la soluzione, la tua risposta mi ha aiutato a capirlo. Aggiungerò la soluzione completa di seguito come risposta separata. Se modifichi il tuo, lo contrassegnerò come accettato. Grazie mille. – barelyknown

+0

Hai dimenticato il "sé". Dovrebbe funzionare. : P –

1

Grazie a Brandon per la risposta che mi ha aiutato a scrivere la soluzione.

Aggiungere quanto segue al modello. Puoi cacheback relazioni parent multiple per modello.È inoltre possibile specificare nomi di attributo diversi per le tabelle padre e figlio passando un hash anziché una stringa per un campo particolare.

include Cacheable 
cacheback(parent: :quotes, fields: %w(weight pallet_spots value equipment_type_id)) 

Questo modulo estende ActiveSupport :: Preoccupazione e aggiunge i callback ed esegue la cacheing. Le tue classi genitore dovranno implementare i metodi calc_field per eseguire il lavoro di memorizzazione nella cache.

module Cacheable 
    extend ActiveSupport::Concern 

    module ClassMethods 
    def cacheback(options) 
     fields = Cacheable.normalize_fields(options[:fields]) 
     [:after_save, :after_destroy].each do |callback| 
     self.send(callback, proc { cache!(fields, self.send(options[:parent])) }) 
     end 
    end 
    end 

    def cache!(fields, objects) 
    objects = objects.respond_to?(:to_a) ? objects.to_a : [objects] 
    objects.each do |object| 
     if object.cacheable? 
     calc(fields, objects) 
     save!(objects) 
     end 
    end 
    end 

    def calc(fields, objects) 
    fields.each do |parent_field, child_field| 
     objects.each(&:"calc_#{parent_field}") if self.send("#{child_field}_changed?".to_sym) 
    end 
    end 

    def save!(objects) 
    objects.each { |object| object.save! if object.changed? } 
    end 

    def self.normalize_fields(fields) 
    Hash[fields.collect { |f| f.is_a?(Hash) ? f.to_a : [f, f] }] 
    end 

end 
+0

potrebbe essere utile: https://github.com/Plinq/big_spoon – apneadiving