如何在更新模型时更新counter_cache?

时间:2011-04-22 17:51:20

标签: ruby-on-rails

我有一个简单的关系:

class Item
  belongs_to :container, :counter_cache => true
end

class Container
  has_many :items
end

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

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

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

8 个答案:

答案 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