2012-02-22 13 views
20

Usando Rails 3.1.3 y estoy tratando de averiguar por qué nuestros cachés de contador no se están actualizando correctamente al cambiar el id. De registro padre a través de atributos_actualización.Rails counter_cache no se actualiza correctamente

class ExhibitorRegistration < ActiveRecord::Base 
    belongs_to :event, :counter_cache => true 
end 

class Event < ActiveRecord::Base 
    has_many :exhibitor_registrations, :dependent => :destroy 
end 

describe ExhibitorRegistration do 
    it 'correctly maintains the counter cache on events' do 
    event = Factory(:event) 
    other_event = Factory(:event) 
    registration = Factory(:exhibitor_registration, :event => event) 

    event.reload 
    event.exhibitor_registrations_count.should == 1 

    registration.update_attributes(:event_id => other_event.id) 

    event.reload 
    event.exhibitor_registrations_count.should == 0 

    other_event.reload 
    other_event.exhibitor_registrations_count.should == 1 
    end 
end 

Esta especificación falla, lo que indica que el contador de la memoria caché en el evento no se está reduciendo.

1) ExhibitorRegistration correctly maintains the counter cache on events 
    Failure/Error: event.exhibitor_registrations_count.should == 0 
    expected: 0 
      got: 1 (using ==) 

¿Debo esperar incluso que esto funcione o debo hacer un seguimiento manual de los cambios y actualizar el contador yo mismo?

Respuesta

43

Desde el fine manual:

: counter_cache

cachés el número de pertenencia objetos de la clase asociada con el uso de increment_counter y decrement_counter. El contador de caché se incrementa cuando un objeto de esta clase se crea y disminuye cuando se destruye.

No hay mención de actualizar la memoria caché cuando un objeto se mueve de un propietario a otro. Por supuesto, la documentación de Rails a menudo está incompleta, así que tendremos que buscar la fuente para confirmarla. Cuando usted dice :counter_cache => true, que trigger a call to the private add_counter_cache_callbacks method y add_counter_cache_callbacks does this:

  1. añade una devolución de llamada after_create que llama a increment_counter.
  2. Agrega una devolución de llamada before_destroy que llama a decrement_counter.
  3. Llama a attr_readonly para que la columna del contador sea de solo lectura.

No creo que espere demasiado, solo está esperando que ActiveRecord sea más completo de lo que es.

No todo está perdido, puede completar las piezas faltantes sin demasiado esfuerzo. Si desea permitir que reparentalización y tienen sus contadores actualizados, se puede añadir una devolución de llamada before_save a su ExhibitorRegistration que ajusta la propia contadores, algo como esto (código de demostración no probado):

class ExhibitorRegistration < ActiveRecord::Base 
    belongs_to :event, :counter_cache => true 
    before_save :fix_counter_cache, :if => ->(er) { !er.new_record? && er.event_id_changed? } 

private 

    def fix_counter_cache 
     Event.decrement_counter(:exhibitor_registration_count, self.event_id_was) 
     Event.increment_counter(:exhibitor_registration_count, self.event_id) 
    end 

end 

Si se va de aventuras, usted podría parche algo así en ActiveRecord::Associations::Builder#add_counter_cache_callbacks y envíe un parche. El comportamiento que espera es razonable y creo que tendría sentido que ActiveRecord lo admitiera.

+1

Gracias @ mu-is-too-short Esto definitivamente soluciona el problema. Creo que esto ciertamente merece atención en ActiveRecord, voy a buscar enviar un parche. –

+0

@MichaelGuterl: Genial, no olvides incluir una actualización de la documentación con tu parche :) –

+0

@MichaelGuterl: Tal vez quieras probar el enfoque de Ben también. Repaso el código de Rails nuevamente para ver si me perdí algo. Esto podría ser solo un error y una documentación pobre/incompleta. –

2

La función counter_cache está diseñada para funcionar a través del nombre de asociación, no la columna subyacente de identificación. En su prueba, en lugar de:

registration.update_attributes(:event_id => other_event.id) 

tratar

registration.update_attributes(:event => other_event) 

Más información se puede encontrar aquí: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

+0

Eso no funcionará, la actualización del contador está ligada a crear y destruir solo para que no se desencadene por un cambio. –

+0

Acabo de verificar doblemente, y esto aumenta y disminuye la columna cexhibitor_registrations_count al modificar el evento a través de atributos_actualización en una instancia de Registro de expositor. Estoy usando Rails 3.0.7 –

+0

¿Te funciona si solo usas la identificación? –

5

Recientemente me encontré con este mismo problema (Rails 3.2.3). Parece que aún no se ha solucionado, así que tuve que seguir adelante y solucionarlo.A continuación se muestra cómo modifiqué ActiveRecord :: Base y utilicé la devolución de llamada after_update para mantener sincronizados mis counter_caches.

Extender ActiveRecord :: Base

Crear un nuevo archivo lib/fix_counters_update.rb con lo siguiente:

module FixUpdateCounters 

    def fix_updated_counters 
    self.changes.each {|key, value| 
     # key should match /master_files_id/ or /bibls_id/ 
     # value should be an array ['old value', 'new value'] 
     if key =~ /_id/ 
     changed_class = key.sub(/_id/, '') 
     changed_class.camelcase.constantize.decrement_counter(:"#{self.class.name.underscore.pluralize}_count", value[0]) unless value[0] == nil 
     changed_class.camelcase.constantize.increment_counter(:"#{self.class.name.underscore.pluralize}_count", value[1]) unless value[1] == nil 
     end 
    } 
    end 
end 

ActiveRecord::Base.send(:include, FixUpdateCounters) 

El código anterior utiliza el método de ActiveModel::Dirtychanges que devuelve un hash que contiene el atributo cambiado y una matriz tanto del valor antiguo como del nuevo valor. Al probar el atributo para ver si se trata de una relación (es decir, termina con/_id /), puede determinar condicionalmente si se debe ejecutar decrement_counter y/o increment_counter. Es esencial probar la presencia de nil en la matriz; de lo contrario, se producirán errores.

Añadir a Inicializadores

Crear un nuevo archivo config/initializers/active_record_extensions.rb con lo siguiente:

require 'fix_update_counters'

Añadir a modelos

Para cada modelo desea que los cachés de contador actualizado Agregar la devolución de llamada:

class Comment < ActiveRecord::Base 
    after_update :fix_updated_counters 
    .... 
end 
2

Si el contador ha sido corrompido o que haya modificado directamente por SQL, puede solucionarlo.

Uso:

ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters) 

Ejemplo 1: Re-calcular el recuento almacenado en caché en el puesto con id = 17.

Post.reset_counters(17, :comments) 

Source

Ejemplo 2: Vuelva a calcular el conteo en caché en todos sus artículos.

Article.ids.each { |id| Article.reset_counters(id, :comments) } 
Cuestiones relacionadas