更新模型时如何更新counter_cache?

我有一个简单的关系:

class Item belongs_to :container, :counter_cache => true end class Container has_many :items end 

假设我有两个容器。 我创建一个项目并将其与第一个容器相关联。 柜台增加了。

然后我决定将它与其他容器相关联。 如何更新两个容器的items_count列?

我在http://railsforum.com/viewtopic.php?id=39285找到了一个可能的解决方案..但是我是初学者,我不明白。 这是唯一的方法吗?

它应该自动工作。 当你更新items.container_id ,它将减少旧容器的计数器并增加新的容器。 但如果它不起作用 – 那很奇怪。 你可以试试这个回调:

 class Item belongs_to :container, :counter_cache => true before_save :update_counters private def update_counters new_container = Container.find self.container_id old_container = Container.find self.container_id_was new_container.increament(:items_count) old_container.decreament(:items_count) end end 

UPD

要演示本机行为:

 container1 = Container.create :title => "container 1" #=> # container2 = Container.create :title => "container 2" #=> # item = container1.items.create(:title => "item 1") Container.first #=> # Container.last #=> # item.container = Container.last item.save Container.first #=> # Container.last #=> # 

所以它应该没有任何黑客攻击。 从箱子里。

修改它以处理自定义计数器缓存名称(不要忘记使用counter_cache将after_update :fix_updated_counter添加到模型中)

 module FixUpdateCounters def fix_updated_counters self.changes.each { |key, (old_value, new_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$/, '' association = self.association changed_class.to_sym case option = association.options[ :counter_cache ] when TrueClass counter_name = "#{self.class.name.tableize}_count" when Symbol counter_name = option.to_s end next unless counter_name association.klass.decrement_counter(counter_name, old_value) if old_value association.klass.increment_counter(counter_name, new_value) if new_value end } end end ActiveRecord::Base.send(:include, FixUpdateCounters) 

对于rails 3.1用户。 使用rails 3.1,答案不起作用。 以下适用于我。

  private def update_counters new_container = Container.find self.container_id Container.increment_counter(:items_count, new_container) if self.container_id_was.present? old_container = Container.find self.container_id_was Container.decrement_counter(:items_count, old_container) end end 

这是一种在类似情况下适合我的方法

 class Item < ActiveRecord::Base after_update :update_items_counts, if: Proc.new { |item| item.collection_id_changed? } private # update the counter_cache column on the changed collections def update_items_counts self.collection_id_change.each do |id| Collection.reset_counters id, :items end end end 

有关脏对象模块的更多信息http://api.rubyonrails.org/classes/ActiveModel/Dirty.html以及关于它们的旧videohttp://railscasts.com/episodes/109-tracking-attribute-changes和reset_counters上的文档http://apidock.com/rails/v3.2.8/ActiveRecord/CounterCache/reset_counters

更新@ fl00r答案

 class Container has_many :items_count end class Item belongs_to :container, :counter_cache => true after_update :update_counters private def update_counters if container_id_changed? Container.increment_counter(:items_count, container_id) Container.decrement_counter(:items_count, container_id_was) end # other counters if any ... ... end end 

我最近遇到了同样的问题(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 

这里@Curley修复了与命名空间模型一起使用的问题。

 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/, '') # Get real class of changed attribute, so work both with namespaced/normal models klass = self.association(changed_class.to_sym).klass # Namespaced model return a slash, split it. unless (counter_name = "#{self.class.name.underscore.pluralize.split("/")[1]}_count".to_sym) counter_name = "#{self.class.name.underscore.pluralize}_count".to_sym end klass.decrement_counter(counter_name, value[0]) unless value[0] == nil klass.increment_counter(counter_name, value[1]) unless value[1] == nil end } end end ActiveRecord::Base.send(:include, FixUpdateCounters) 

对不起,我没有足够的声誉来评论答案。
关于fl00r,我可能会看到一个问题,如果有错误并保存返回“false”,计数器已经更新但它应该没有更新。 所以我想知道“after_update:update_counters”是否更合适。

Curley的答案有效,但如果你在我的情况下,要小心,因为它会检查所有列“_id”。 在我的情况下,它会自动更新我不想更新的字段。

这是另一个建议(几乎类似于Satish):

 def update_counters if container_id_changed? Container.increment_counter(:items_count, container_id) unless container_id.nil? Container.decrement_counter(:items_count, container_id_was) unless container_id_was.nil? end end