2012-02-22 76 views
20

使用Rails的3.1.3,我想弄清楚爲什麼我們的計數器緩存沒有正確更新時通過update_attributes更改父記錄的id。Rails的counter_cache沒有正確更新

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

我應該甚至期望這個工作或我需要手動跟蹤更改並更新自己的櫃檯?

回答

43

fine manual

:counter_cache

緩存通過使用increment_counterdecrement_counter屬於對關聯類對象的數量。計數器緩存在創建該類的對象時遞增,並在其被銷燬時遞減。

當對象從一個所有者移動到另一個時,沒有提到更新緩存。當然,Rails文檔通常不完整,所以我們必須查看源代碼進行確認。當你說:counter_cache => true,你trigger a call to the private add_counter_cache_callbacks methodadd_counter_cache_callbacks does this

  1. 添加一個after_create回調這就要求increment_counter
  2. 添加一個before_destroy回調,其中調用decrement_counter
  3. 調用attr_readonly使計數器列只讀。

我不認爲你期待太多,你只是希望ActiveRecord比它更完整。

儘管沒有丟失,但您可以自己填寫缺失的部分,而不需要太多的努力。如果要允許重排根,有你的櫃檯更新,你可以添加一個before_save回調到您的ExhibitorRegistration,調整計數器本身,像這樣(未經測試的演示代碼):

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支持它是有意義的。

+1

Thanks @ mu-is-too-short這絕對可以解決這個問題。我認爲這在ActiveRecord本身當然值得關注,我會考慮提交補丁。 – 2012-02-23 13:02:25

+0

@MichaelGuterl:很酷,別忘了在你的補丁中包含一個文檔更新:) – 2012-02-23 14:48:28

+0

@MichaelGuterl:你也可以嘗試Ben的方法。我要再次檢查Rails代碼,看看我是否錯過了任何東西。這可能只是一個錯誤和糟糕/不完整的文檔。 – 2012-02-24 04:22:22

2

counter_cache函數用於處理關聯名稱,而不是基礎標識列。在您的測試,而不是:

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

嘗試

registration.update_attributes(:event => other_event) 

更多信息可以在這裏找到:http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

+0

這將無法正常工作,計數器更新與創建和銷燬有關,因此它們不會被更改觸發。 – 2012-02-23 08:28:44

+0

我剛剛檢查了兩次,這是在ExhibitorRegistration實例上通過update_attributes修改事件時遞增和遞減cexhibitor_registrations_count列。我正在使用Rails 3.0.7 – 2012-02-24 02:19:22

+0

如果您只使用ID,它是否適用於您? – 2012-02-24 04:17:13

5

我最近遇到同樣的問題(Rails的3.2.3)來了。看起來它還沒有被修復,所以我不得不繼續修復。以下是我如何修改ActiveRecord :: Base並利用after_update回調來保持我的counter_caches同步。

擴展的ActiveRecord ::基地

具有以下創建一個新文件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 
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) }