更新模型时如何更新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