【问题标题】:Rails counter_cache not updating correctlyRails counter_cache 未正确更新
【发布时间】:2012-02-22 21:45:37
【问题描述】:

使用 Rails 3.1.3,我试图弄清楚为什么我们的计数器缓存在通过 update_attributes 更改父记录 ID 时没有正确更新。

class ExhibitorRegistration < ActiveRecord::Base
  belongs_to :event, :counter_cache => true
end

class Event < ActiveRecord::Base
  has_many :exhibitor_registrations, :dependent => :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 ==)

我是否应该期望这能正常工作,还是我需要手动跟踪更改并自己更新计数器?

【问题讨论】:

    标签: ruby-on-rails ruby-on-rails-3.1 rails-activerecord


    【解决方案1】:

    来自fine manual

    :counter_cache

    通过使用increment_counterdecrement_counter 缓存关联类上的所属对象的数量。计数器缓存在创建此类对象时递增,在销毁时递减。

    没有提到当对象从一个所有者移动到另一个所有者时更新缓存。当然,Rails 文档通常不完整,因此我们必须查看源代码以确认。当你说:counter_cache =&gt; true,你trigger a call to the private add_counter_cache_callbacks methodadd_counter_cache_callbacks does this

    1. 添加一个调用increment_counterafter_create回调。
    2. 添加一个调用decrement_counterbefore_destroy回调。
    3. 调用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 支持它是有意义的。

    【讨论】:

    • 谢谢@mu-is-too-short 这绝对解决了这个问题。我认为这在 ActiveRecord 本身中确实值得关注,我会考虑提交补丁。
    • @MichaelGuterl:很酷,别忘了在你的补丁中包含一个文档更新:)
    • @MichaelGuterl:你可能也想试试 Ben 的方法。我再次浏览 Rails 代码,看看我是否遗漏了什么。这可能只是一个错误和糟糕/不完整的文档。
    • @Pierre:还有lambda { |er| ... }symbolhound.com 有时会有所帮助(但并非总是如此)。
    • 这个答案很有帮助。我还要提一下,读者应该注意,如果他们的 counter_cache 关系(我们称之为 xyz)是多态的,他们应该:1. 如果 xyz_id 或 xyz_type 列发生变化,请确保 fix_counter_cache,2. 记得在xyz_type_was 表示的类,xyz_type 表示的类的 increment_counter。
    【解决方案2】:

    如果您的计数器已损坏或您已直接通过 SQL 修改它,您可以修复它。

    使用:

    ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)
    

    示例 1:重新计算 id = 17 的帖子的缓存计数。

    Post.reset_counters(17, :comments)
    

    Source

    示例 2:重新计算所有文章的缓存计数。

    Article.ids.each { |id| Article.reset_counters(id, :comments) }
    

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        对此的修复已合并到活动记录主文件中

        https://github.com/rails/rails/issues/9722

        【讨论】:

        • 需要注意的是,该修复仅适用于 Rails 4.0 及更高版本,问题针对的是 Rails 3.1。
        【解决方案5】:

        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

        【讨论】:

        • 那行不通,计数器更新只与创建和销毁相关联,因此它们不会被更改触发。
        • 我只是仔细检查了一遍,这是在通过 ExhibitorRegistration 实例上的 update_attributes 修改事件时递增和递减 cexhibitor_registrations_count 列。我正在使用 Rails 3.0.7
        • 如果您查看ActiveRecord::Associations::Builder::BelongsTo#add_counter_cache_callbacks,您就会明白我在说什么。但是,您会在 ActiveRecord::Associations::BelongsToAssociation#update_counters 中看到更多的计数器摆弄。我想知道是否有两个不太一致的单独代码路径。
        • @muistooshort 一旦我开始尝试将补丁应用到 Rails,我看到了同样的事情。但是,我想保持 update_attributes 在控制器中工作,而不必显式找到事件并将其添加到参数中。我仍在研究如何对 Rails 进行适当的修补,但我还需要几天的时间才能对事情做出准确的评估。
        • @MichaelGuterl:我的看法是,同一事物有两个不同的代码路径,而仅 id 路径不完整。看起来像一个 2 部分错误:文档不正确,因为它没有说明“更改”情况,并且仅 id 路径不完整,因为它不处理“更改”情况。
        猜你喜欢
        • 1970-01-01
        • 2017-06-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-31
        • 1970-01-01
        • 2021-10-26
        相关资源
        最近更新 更多