Подтвердить что ты не робот

Rails counter_cache не обновляется правильно

Использование 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 ==)

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

4b9b3361

Ответ 1

Из точное руководство:

: counter_cache

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

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

  • Добавляет обратный вызов after_create, который вызывает increment_counter.
  • Добавляет обратный вызов before_destroy, который вызывает decrement_counter.
  • Вызывает 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 было бы разумно его поддерживать.

Ответ 2

Недавно я столкнулся с этой проблемой (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

Функция counter_cache предназначена для работы через имя ассоциации, а не в столбце базового идентификатора. В вашем тесте вместо

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

попробовать

registration.update_attributes(:event => other_event)

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

Ответ 5

Если ваш счетчик был поврежден или вы изменили его напрямую 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) }