我有一个简单的关系:
class Item
belongs_to :container, :counter_cache => true
end
class Container
has_many :items
end
假设我有两个容器。我创建一个项目并将其与第一个容器相关联。柜台增加了。
然后我决定将它与其他容器相关联。如何更新两个容器的items_count列?
我在http://railsforum.com/viewtopic.php?id=39285找到了一个可能的解决方案..但是我是初学者,我不明白。这是唯一的方法吗?
答案 0 :(得分:3)
它应该自动运行。当你更新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"
#=> #<Container title: "container 1", :items_count: nil>
container2 = Container.create :title => "container 2"
#=> #<Container title: "container 2", :items_count: nil>
item = container1.items.create(:title => "item 1")
Container.first
#=> #<Container title: "container 1", :items_count: 1>
Container.last
#=> #<Container title: "container 1", :items_count: nil>
item.container = Container.last
item.save
Container.first
#=> #<Container title: "container 1", :items_count: 0>
Container.last
#=> #<Container title: "container 1", :items_count: 1>
所以它应该没有任何黑客行为。从框中。
答案 1 :(得分:3)
修改它以处理自定义计数器缓存名称
(不要忘记使用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)
答案 2 :(得分:2)
对于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
答案 3 :(得分:2)
这是一种在类似情况下适合我的方法
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的详细信息及有关它们的旧视频http://railscasts.com/episodes/109-tracking-attribute-changes以及有关reset_counters http://apidock.com/rails/v3.2.8/ActiveRecord/CounterCache/reset_counters的文档
答案 4 :(得分:1)
更新@ 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
答案 5 :(得分:1)
我最近遇到了同样的问题(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
答案 6 :(得分:1)
@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)
答案 7 :(得分:1)
抱歉,我没有足够的声誉来评论答案 关于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