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 ==)
これが機能することを期待する必要がありますか、それとも手動で変更を追跡して自分でカウンターを更新する必要がありますか?
細かいマニュアル から:
:counter_cache
increment_counter
およびdecrement_counter
を使用して、関連クラスに属するオブジェクトの数をキャッシュします。カウンターキャッシュは、このクラスのオブジェクトが作成されるとインクリメントされ、破棄されるとデクリメントされます。
オブジェクトがある所有者から別の所有者に移動されたときにキャッシュを更新することについては言及されていません。もちろん、Railsのドキュメントは不完全であることが多いため、確認のためにソースを確認する必要があります。:counter_cache => true
と言うと、 トリガーします。プライベートadd_counter_cache_callbacks
メソッド の呼び出しと add_counter_cache_callbacks
はこれを行います :
after_create
を呼び出すincrement_counter
コールバックを追加します。before_destroy
を呼び出すdecrement_counter
コールバックを追加します。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がそれをサポートすることは理にかなっていると思います。
カウンターが破損している場合、または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) }
私は最近、これと同じ問題に遭遇しました(Rails3.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
これに対する修正がアクティブレコードマスターにマージされました
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