Rails counter_cache not updating correctly

前端 未结 5 682
佛祖请我去吃肉
佛祖请我去吃肉 2021-02-04 00:00

Using Rails 3.1.3 and I\'m trying to figure out why our counter caches aren\'t being updated correctly when changing the parent record id via update_attributes.

         


        
相关标签:
5条回答
  • 2021-02-04 00:33

    If your counter has been corrupted or you've modified it directly by SQL, you can fix it.

    Using:

    ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)
    

    Example 1: Re-compute the cached count on the post with id = 17.

    Post.reset_counters(17, :comments)
    

    Source

    Example 2: Re-compute the cached count on all your articles.

    Article.ids.each { |id| Article.reset_counters(id, :comments) }
    
    0 讨论(0)
  • 2021-02-04 00:37

    The counter_cache function is designed to work through the association name, not the underlying id column. In your test, instead of:

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

    try

    registration.update_attributes(:event => other_event)
    

    More information can be found here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

    0 讨论(0)
  • 2021-02-04 00:46

    From the fine manual:

    :counter_cache

    Caches the number of belonging objects on the associate class through the use of increment_counter and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it’s destroyed.

    There's no mention of updating the cache when an object is moved from one owner to another. Of course, the Rails documentation is often incomplete so we'll have to look at the source for confirmation. When you say :counter_cache => true, you trigger a call to the private add_counter_cache_callbacks method and add_counter_cache_callbacks does this:

    1. Adds an after_create callback which calls increment_counter.
    2. Adds an before_destroy callback which calls decrement_counter.
    3. Calls attr_readonly to make the counter column readonly.

    I don't think you're expecting too much, you're just expecting ActiveRecord to be more complete than it is.

    All is not lost though, you can fill in the missing pieces yourself without too much effort. If you want to allow reparenting and have your counters updated, you can add a before_save callback to your ExhibitorRegistration that adjusts the counters itself, something like this (untested demo code):

    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
    

    If you were adventurous, you could patch something like that into ActiveRecord::Associations::Builder#add_counter_cache_callbacks and submit a patch. The behavior you're expecting is reasonable and I think it would make sense for ActiveRecord to support it.

    0 讨论(0)
  • 2021-02-04 00:49

    A fix for this has been merged in to active record master

    https://github.com/rails/rails/issues/9722

    0 讨论(0)
  • 2021-02-04 00:53

    I recently came across this same problem (Rails 3.2.3). Looks like it has yet to be fixed, so I had to go ahead and make a fix. Below is how I amended ActiveRecord::Base and utilize after_update callback to keep my counter_caches in sync.

    Extend ActiveRecord::Base

    Create a new file lib/fix_counters_update.rb with the following:

    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)
    

    The above code uses the ActiveModel::Dirty method changes which returns a hash containing the attribute changed and an array of both the old value and new value. By testing the attribute to see if it is a relationship (i.e. ends with /_id/), you can conditionally determine whether decrement_counter and/or increment_counter need be run. It is essnetial to test for the presence of nil in the array, otherwise errors will result.

    Add to Initializers

    Create a new file config/initializers/active_record_extensions.rb with the following:

    require 'fix_update_counters'

    Add to models

    For each model you want the counter caches updated add the callback:

    class Comment < ActiveRecord::Base
      after_update :fix_updated_counters
      ....
    end
    
    0 讨论(0)
提交回复
热议问题