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 ==)
Должен ли я ожидать, что это сработает или мне нужно вручную отслеживать изменения и обновлять счетчик самостоятельно?
Ответы
Ответ 1
Из точное руководство:
: counter_cache
Закрепляет количество принадлежащих объектов в ассоциированном классе с помощью increment_counter
и decrement_counter
. Счетчик кеша увеличивается, когда объект этого класса создается и уменьшается при его уничтожении.
Нет упоминания об обновлении кеша, когда объект перемещается от одного владельца к другому. Конечно, документация Rails часто неполна, поэтому нам нужно посмотреть источник для подтверждения. Когда вы говорите :counter_cache => true
, вы запускаете вызов частного add_counter_cache_callbacks
метода и add_counter_cache_callbacks
делает это:
Я не думаю, что вы ожидаете слишком многого, вы просто ожидаете, что 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
Ответ 3
Исправление для этого было объединено с активным мастером записи
https://github.com/rails/rails/issues/9722
Ответ 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) }