Rails counter_cache не обновляется корректно - PullRequest
21 голосов
/ 23 февраля 2012

Использование Rails 3.1.3, и я пытаюсь выяснить, почему наши кэши счетчиков не обновляются правильно при изменении идентификатора родительской записи с помощью update_attributes.

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

Эта спецификация не работает, указывая, чтоКэш счетчика для события не уменьшается.

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

Стоит ли ожидать, что это сработает, или мне нужно вручную отслеживать изменения и обновлять счетчик самостоятельно?

Ответы [ 5 ]

48 голосов
/ 23 февраля 2012

Из тонкой инструкции :

: counter_cache

Кэширует количество принадлежащих объектов в ассоциированном классе с помощью increment_counter и decrement_counter. Кэш счетчика увеличивается при создании объекта этого класса и уменьшается при уничтожении.

Нет упоминания об обновлении кэша при перемещении объекта от одного владельца к другому. Конечно, документация по Rails часто бывает неполной, поэтому нам придется искать источник для подтверждения. Когда вы говорите :counter_cache => true, вы запускаете вызов частного add_counter_cache_callbacks метода , а add_counter_cache_callbacks делает это :

  1. Добавляет after_create обратный вызов, который вызывает increment_counter.
  2. Добавляет before_destroy обратный вызов, который вызывает decrement_counter.
  3. Вызывает attr_readonly, чтобы сделать счетчик только для чтения.

Не думаю, что вы ожидаете слишком многого, вы просто ожидаете, что ActiveRecord будет более полным, чем он есть.

Однако еще не все потеряно, вы можете заполнить недостающие фрагменты самостоятельно без особых усилий. Если вы хотите разрешить родительские функции и обновить свои счетчики, вы можете добавить обратный вызов before_save в свою учетную запись ExhibitorRegistration, которая настраивает сами счетчики, примерно так (непроверенный демонстрационный код):

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

Если бы вы были в авантюре, вы могли бы внести что-то подобное в ActiveRecord::Associations::Builder#add_counter_cache_callbacks и отправить патч. Вы ожидаете, что поведение будет разумным, и я думаю, что ActiveRecord имеет смысл его поддерживать.

5 голосов
/ 02 мая 2012

Недавно я столкнулся с этой же проблемой (Rails 3.2.3). Похоже, что это еще не исправлено, поэтому я должен был пойти дальше и исправить. Ниже показано, как я изменил ActiveRecord :: Base и использую обратный вызов after_update для синхронизации моих counter_caches.

Расширение ActiveRecord :: Base

Создайте новый файл lib/fix_counters_update.rb со следующим:

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)

В приведенном выше коде используется метод ActiveModel :: Dirty changes, который возвращает хеш, содержащий измененный атрибут и массив как старого, так и нового значения. Протестировав атрибут, чтобы определить, является ли это отношением (т.е. оканчивается на / _id /), вы можете условно определить, нужно ли запускать decrement_counter и / или increment_counter. Необходимо проверить наличие nil в массиве, в противном случае возникнут ошибки.

Добавить к инициализаторам

Создайте новый файл config/initializers/active_record_extensions.rb со следующим:

require 'fix_update_counters'

Добавить к моделям

Для каждой модели, которую вы хотите обновить кэши счетчиков, добавьте обратный вызов:

class Comment < ActiveRecord::Base
  after_update :fix_updated_counters
  ....
end
4 голосов
/ 16 марта 2013

Исправление для этого было объединено с активной основной записью

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

3 голосов
/ 23 февраля 2016

Если ваш счетчик был поврежден или вы изменили его напрямую с помощью SQL, вы можете исправить это.

Использование:

ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)

Пример 1. Пересчитать количество кешированных сообщений в сообщении с id = 17.

Post.reset_counters(17, :comments)

Источник

Пример 2: пересчитать количество кешированных записей для всех ваших статей.

Article.ids.each { |id| Article.reset_counters(id, :comments) }
2 голосов
/ 23 февраля 2012

Функция counter_cache предназначена для работы через имя ассоциации, а не нижележащий столбец id.В вашем тесте вместо:

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

try

registration.update_attributes(:event => other_event)

Более подробную информацию можно найти здесь: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

...