Rails counter_cache 未正确更新

发布于 2025-01-08 00:06:59 字数 1122 浏览 0 评论 0 原文

使用 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 ==)

我是否应该期望这能起作用,或者我是否需要手动跟踪更改并自己更新计数器?

Using Rails 3.1.3 and I'm trying to figure out why our counter caches aren't being updated correctly when changing the parent record id via update_attributes.

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

This spec fails indicating that the counter cache on event is not being decremented.

1) ExhibitorRegistration correctly maintains the counter cache on events
   Failure/Error: event.exhibitor_registrations_count.should == 0
     expected: 0
          got: 1 (using ==)

Should I even expect this to work or do I need to manually track the changes and update the counter myself?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

夜清冷一曲。 2025-01-15 00:06:59

来自精细手册

:counter_cache

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

没有提到当对象从一个所有者移动到另一个所有者时更新缓存。当然,Rails 文档通常不完整,因此我们必须查看源代码进行确认。当你说 :counter_cache =>; true,您触发对私人的呼叫add_counter_cache_callbacks 方法add_counter_cache_callbacks 执行此操作

  1. 添加一个 after_create 回调,该回调调用 increment_counter
  2. 添加一个 before_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 支持它是有意义的。

From the fine manual:

:counter_cache

Caches the number of belonging objects on the associate class through the use of increment_counter and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it’s destroyed.

There's no mention of updating the cache when an object is moved from one owner to another. Of course, the Rails documentation is often incomplete so we'll have to look at the source for confirmation. When you say :counter_cache => true, you trigger a call to the private add_counter_cache_callbacks method and add_counter_cache_callbacks does this:

  1. Adds an after_create callback which calls increment_counter.
  2. Adds an before_destroy callback which calls decrement_counter.
  3. Calls attr_readonly to make the counter column readonly.

I don't think you're expecting too much, you're just expecting ActiveRecord to be more complete than it is.

All is not lost though, you can fill in the missing pieces yourself without too much effort. If you want to allow reparenting and have your counters updated, you can add a before_save callback to your ExhibitorRegistration that adjusts the counters itself, something like this (untested demo code):

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

If you were adventurous, you could patch something like that into ActiveRecord::Associations::Builder#add_counter_cache_callbacks and submit a patch. The behavior you're expecting is reasonable and I think it would make sense for ActiveRecord to support it.

记忆里有你的影子 2025-01-15 00:06:59

如果您的计数器已损坏或者您直接通过 SQL 对其进行了修改,则可以修复它。

使用:

ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)

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

Post.reset_counters(17, :comments)

来源

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

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

If your counter has been corrupted or you've modified it directly by SQL, you can fix it.

Using:

ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)

Example 1: Re-compute the cached count on the post with id = 17.

Post.reset_counters(17, :comments)

Source

Example 2: Re-compute the cached count on all your articles.

Article.ids.each { |id| Article.reset_counters(id, :comments) }
等风也等你 2025-01-15 00:06:59

我最近遇到了同样的问题(Rails 3.2.3)。看起来它还没有修复,所以我必须继续进行修复。下面是我如何修改 ActiveRecord::Base 并利用 after_update 回调来保持 counter_cache 同步。

扩展 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'

添加到模型< /strong>

对于您想要更新计数器缓存的每个模型,添加回调:

class Comment < ActiveRecord::Base
  after_update :fix_updated_counters
  ....
end

I recently came across this same problem (Rails 3.2.3). Looks like it has yet to be fixed, so I had to go ahead and make a fix. Below is how I amended ActiveRecord::Base and utilize after_update callback to keep my counter_caches in sync.

Extend ActiveRecord::Base

Create a new file lib/fix_counters_update.rb with the following:

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)

The above code uses the ActiveModel::Dirty method changes which returns a hash containing the attribute changed and an array of both the old value and new value. By testing the attribute to see if it is a relationship (i.e. ends with /_id/), you can conditionally determine whether decrement_counter and/or increment_counter need be run. It is essnetial to test for the presence of nil in the array, otherwise errors will result.

Add to Initializers

Create a new file config/initializers/active_record_extensions.rb with the following:

require 'fix_update_counters'

Add to models

For each model you want the counter caches updated add the callback:

class Comment < ActiveRecord::Base
  after_update :fix_updated_counters
  ....
end
预谋 2025-01-15 00:06:59

此问题的修复已合并到活动记录主

https://github.com/rails/rails /问题/9722

A fix for this has been merged in to active record master

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

生生漫 2025-01-15 00:06:59

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

The counter_cache function is designed to work through the association name, not the underlying id column. In your test, instead of:

registration.update_attributes(:event_id => other_event.id)

try

registration.update_attributes(:event => other_event)

More information can be found here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文