2011-04-22 13 views
5

que tienen una relación simple:¿Cómo actualizar counter_cache al actualizar un modelo?

class Item 
    belongs_to :container, :counter_cache => true 
end 

class Container 
    has_many :items 
end 

Digamos que tengo dos contenedores. Creo un artículo y lo asocio con el primer contenedor. El contador está aumentado.

Luego decido asociarlo con el otro contenedor. ¿Cómo actualizar la columna items_count de ambos contenedores?

Encontré una posible solución en http://railsforum.com/viewtopic.php?id=39285 .. sin embargo, soy un principiante y no lo entiendo. ¿Es esta la única manera de hacerlo?

Respuesta

3

Debería funcionar automáticamente. Cuando actualice items.container_id, decrementará el contador del contenedor antiguo y el nuevo. Pero si no funciona, es extraño. Usted puede tratar de esta devolución de llamada:

class Item 
    belongs_to :container, :counter_cache => true 
    before_save :update_counters 

    private 
    def update_counters 
    new_container = Container.find self.container_id 
    old_container = Container.find self.container_id_was 
    new_container.increament(:items_count) 
    old_container.decreament(:items_count) 
    end 
end 

UPD

Para demostrar el comportamiento nativo:

container1 = Container.create :title => "container 1" 
#=> #<Container title: "container 1", :items_count: nil> 
container2 = Container.create :title => "container 2" 
#=> #<Container title: "container 2", :items_count: nil> 
item = container1.items.create(:title => "item 1") 
Container.first 
#=> #<Container title: "container 1", :items_count: 1> 
Container.last 
#=> #<Container title: "container 1", :items_count: nil> 
item.container = Container.last 
item.save 
Container.first 
#=> #<Container title: "container 1", :items_count: 0> 
Container.last 
#=> #<Container title: "container 1", :items_count: 1> 

así que debería funcionar sin ningún tipo de piratería informática. De la caja.

2

Para rieles 3.1 usuarios. Con rieles 3.1, la respuesta no funciona. Lo siguiente me funciona.

private 
    def update_counters 
     new_container = Container.find self.container_id 
     Container.increment_counter(:items_count, new_container) 
     if self.container_id_was.present? 
     old_container = Container.find self.container_id_was 
     Container.decrement_counter(:items_count, old_container) 
     end 
    end 
1

Actualizaciones para fl00r @ Respuesta

class Container 
    has_many :items_count 
end 

class Item 
    belongs_to :container, :counter_cache => true 
    after_update :update_counters 

    private 

def update_counters 
    if container_id_changed? 
    Container.increment_counter(:items_count, container_id) 
    Container.decrement_counter(:items_count, container_id_was) 
    end 

    # other counters if any 
    ... 
    ... 

end 

end 
+0

que probablemente será un error si container_id/container_id_was es nula. – Zequez

1

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 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 
1

Aquí la corrección de @Curley funciona con modelos de espacio de nombres.

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/, '') 

     # Get real class of changed attribute, so work both with namespaced/normal models 
     klass = self.association(changed_class.to_sym).klass 

     # Namespaced model return a slash, split it. 
     unless (counter_name = "#{self.class.name.underscore.pluralize.split("/")[1]}_count".to_sym) 
      counter_name = "#{self.class.name.underscore.pluralize}_count".to_sym 
     end 

     klass.decrement_counter(counter_name, value[0]) unless value[0] == nil 
     klass.increment_counter(counter_name, value[1]) unless value[1] == nil 
     end 
    } 
    end 
end 

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

modificado un poco para manejar nombres de caché custom counter (no se olvide de añadir after_update :fix_updated_counter a los modelos que utilizan counter_cache)

module FixUpdateCounters 

    def fix_updated_counters 
    self.changes.each { |key, (old_value, new_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$/, '' 
     association = self.association changed_class.to_sym 

     case option = association.options[ :counter_cache ] 
     when TrueClass 
      counter_name = "#{self.class.name.tableize}_count" 
     when Symbol 
      counter_name = option.to_s 
     end 

     next unless counter_name 

     association.klass.decrement_counter(counter_name, old_value) if old_value 
     association.klass.increment_counter(counter_name, new_value) if new_value 
     end 
    } end end 

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

Esta respuesta es la más dinámica y sería mejor para proyectos con muchos cachés de contador que incluyen algunos nombres personalizados. Para Martin, la respuesta aceptada puede ser más fácil. –

2

aquí es un enfoque que funciona bien para mí en semejante situaciones

class Item < ActiveRecord::Base 

    after_update :update_items_counts, if: Proc.new { |item| item.collection_id_changed? } 

private 

    # update the counter_cache column on the changed collections 
    def update_items_counts 

     self.collection_id_change.each do |id| 
      Collection.reset_counters id, :items 
     end 

    end 

end 

información adicional sobre el módulo objeto sucio http://api.rubyonrails.org/classes/ActiveModel/Dirty.html y un viejo video sobre ellos http://railscasts.com/episodes/109-tracking-attribute-changes y documentación sobre reset_counters http://apidock.com/rails/v3.2.8/ActiveRecord/CounterCache/reset_counters

1

Lo siento, no tengo la reputación suficiente para comentar las respuestas.
Sobre fl00r, puedo ver un problema si hay un error y guardar el resultado "falso", el contador ya se ha actualizado pero no debería haber sido actualizado. Entonces me pregunto si "after_update: update_counters" es más apropiado.

La respuesta de Curley funciona, pero si usted está en mi caso, tenga cuidado porque comprobará todas las columnas con "_id". En mi caso, está actualizando automáticamente un campo que no quiero que se actualice.

Aquí es otra sugerencia (casi similar a Satish):

def update_counters 
    if container_id_changed? 
    Container.increment_counter(:items_count, container_id) unless container_id.nil? 
    Container.decrement_counter(:items_count, container_id_was) unless container_id_was.nil? 
    end 
end 
Cuestiones relacionadas