Rails counter_cache没有正确更新
使用Rails 3.1.3,我试图弄清楚为什么我们的计数器缓存在通过update_attributes更改父记录ID时没有正确更新。
class ExhibitorRegistration true end class Event :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 ==)
我是否应该期望这个工作或我是否需要手动跟踪更改并自行更新计数器?
从精细手册 :
:counter_cache
通过使用
increment_counter
和decrement_counter
缓存关联类上的所属对象的数量。 计数器缓存在创建此类的对象时递增,在销毁时递减。
当对象从一个所有者移动到另一个所有者时,没有提到更新缓存。 当然,Rails文档通常是不完整的,因此我们必须查看源代码以进行确认。 当你说:counter_cache => true
,你会触发对私有add_counter_cache_callbacks
方法的调用, add_counter_cache_callbacks
add_counter_cache_callbacks
:
- 添加一个调用
increment_counter
的after_create
回调。 - 添加一个调用
decrement_counter
的before_destroy
回调。 - 调用
attr_readonly
使计数器列只读。
我不认为你期望太多,你只是期望ActiveRecord比它更完整。
一切都不会丢失,你可以自己填写缺失的部分而不需要太多的努力。 如果您想允许重新显示并更新计数器,您可以向ExhibitorRegistration添加before_save
回调,以调整计数器本身,如下所示(未经测试的演示代码):
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支持它是有意义的。
我最近遇到了同样的问题(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
已修复此问题已合并到活动记录主文件中
如果您的计数器已损坏或您已通过SQL直接修改它,则可以修复它。
使用:
ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)
示例1:重新计算id = 17的post上的缓存计数。
Post.reset_counters(17, :comments)
资源
示例2:重新计算所有文章的缓存计数。
Article.ids.each { |id| Article.reset_counters(id, :comments) }
counter_cache函数用于处理关联名称,而不是基础id列。 在您的测试中,而不是:
registration.update_attributes(:event_id => other_event.id)
尝试
registration.update_attributes(:event => other_event)
更多信息可以在这里找到: http : //api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html