web-dev-qa-db-ja.com

Rails counter_cacheが正しく更新されていません

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

これが機能することを期待する必要がありますか、それとも手動で変更を追跡して自分でカウンターを更新する必要がありますか?

23
Michael Guterl

細かいマニュアル から:

:counter_cache

increment_counterおよびdecrement_counterを使用して、関連クラスに属するオブジェクトの数をキャッシュします。カウンターキャッシュは、このクラスのオブジェクトが作成されるとインクリメントされ、破棄されるとデクリメントされます。

オブジェクトがある所有者から別の所有者に移動されたときにキャッシュを更新することについては言及されていません。もちろん、Railsのドキュメントは不完全であることが多いため、確認のためにソースを確認する必要があります。:counter_cache => trueと言うと、 トリガーします。プライベートadd_counter_cache_callbacksメソッド の呼び出しと add_counter_cache_callbacksはこれを行います

  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がそれをサポートすることは理にかなっていると思います。

50
mu is too short

カウンターが破損している場合、または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) }
5
Is Ma

私は最近、これと同じ問題に遭遇しました(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
5
Curley

これに対する修正がアクティブレコードマスターにマージされました

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

4
msaspence

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

2
Ben Simpson