2012-02-22 14 views
20

Używanie Rails 3.1.3 i próbuję dowiedzieć się, dlaczego nasze pamięci podręczne liczników nie są poprawnie aktualizowane po zmianie identyfikatora rekordu nadrzędnego za pomocą update_attributes.Railsy counter_cache nie aktualizują się prawidłowo

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 

Ta specyfikacja nie powiedzie się, że licznik pamięci podręcznej zdarzenia nie jest zmniejszany.

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

Czy powinienem się spodziewać, że to zadziała, czy muszę ręcznie śledzić zmiany i samemu aktualizować licznik?

Odpowiedz

43

Z fine manual:

: counter_cache

buforuje liczba przynależności przedmiotów na stowarzyszonego klasie dzięki zastosowaniu increment_counter i decrement_counter. Pamięć podręczna licznika jest zwiększana, gdy obiekt tej klasy zostanie utworzony i zmniejszony, gdy zostanie zniszczony.

Nie ma wzmianki o aktualizację cache, gdy obiekt porusza się od jednego właściciela do drugiego. Oczywiście, dokumentacja Rails jest często niekompletna, więc będziemy musieli sprawdzić źródło w celu potwierdzenia. Kiedy mówisz :counter_cache => true, ty trigger a call to the private add_counter_cache_callbacks method i add_counter_cache_callbacks does this:

  1. Dodaje after_create zwrotnego, który wywołuje increment_counter.
  2. Dodaje wywołanie zwrotne before_destroy, które wywołuje decrement_counter.
  3. Wywołuje attr_readonly, aby ustawić kolumnę licznika tylko do odczytu.

Nie sądzę, że oczekujesz zbyt wiele, tylko oczekujesz, że ActiveRecord będzie bardziej kompletny niż jest.

Nie wszystko stracone, sam możesz uzupełnić brakujące elementy bez większego wysiłku. Jeśli chcesz, aby umożliwić reparenting i zaktualizowaniu liczniki, można dodać before_save oddzwanianie do ExhibitorRegistration że reguluje się sama liczniki, coś takiego (niesprawdzone kod demo):

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 

Gdybyś ryzykowny, można załatw coś podobnego w ActiveRecord::Associations::Builder#add_counter_cache_callbacks i prześlij łatkę. Zachowanie, którego się spodziewasz, jest rozsądne i myślę, że sensowne byłoby, aby ActiveRecord je wspierał.

+1

Dzięki @ mu-is-too-short to zdecydowanie rozwiązuje problem. Myślę, że to na pewno zasługuje na uwagę w samym ActiveRecord, przyjrzę się złożeniu łatki. –

+0

@MichaelGuterl: Fajnie, nie zapomnij dołączyć aktualizacji dokumentacji ze swoim patchem :) –

+0

@MichaelGuterl: Możesz również spróbować podejścia Bena. Przechodzę ponownie przez kod Railsowy, aby zobaczyć, czy coś przeoczyłem. Może to być po prostu błąd i słaba/niekompletna dokumentacja. –

2

Funkcja counter_cache jest zaprojektowana do pracy poprzez nazwę asocjacji, a nie bazową kolumnę id. W swoim badaniu, zamiast:

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

spróbować

registration.update_attributes(:event => other_event) 

Więcej informacji można znaleźć tutaj: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

+0

To nie zadziała, aktualizacja licznika jest powiązana z tworzeniem i niszczeniem tylko po to, aby nie zostały wywołane przez zmianę. –

+0

Właśnie dwukrotnie sprawdziłem, i to jest inkrementowanie i zmniejszanie kolumny cexhibitor_registrations_count podczas modyfikowania zdarzenia poprzez update_attributes na wystąpieniu ExhibitorRegistration. Używam Rails 3.0.7 –

+0

Czy to działa, jeśli używasz tylko ID? –

5

Niedawno natknąłem się na ten sam problem (Rails 3.2.3). Wygląda na to, że jeszcze nie zostało naprawione, więc musiałem iść dalej i naprawić.Poniżej opisano, jak zmieniłem ActiveRecord :: Base i wykorzystuję callback after_update, aby zachować synchronizację counter_cache.

Extend ActiveRecord :: Base

Utwórz nowy plik lib/fix_counters_update.rb z następujących czynności:

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) 

Powyższy kod wykorzystuje metodę changesActiveModel::Dirty która zwraca hash zawierający atrybut zmieniły i tablicę zarówno starej wartości, jak i nowej wartości. Testując atrybut, aby zobaczyć, czy jest to relacja (tj. Kończy się na/_id /), można warunkowo określić, czy należy uruchomić decrement_counter i/lub increment_counter. Należy przetestować obecność w macierzy wartości nil, w przeciwnym razie wystąpią błędy.

Dodaj do inicjalizatory

Utwórz nowy plik config/initializers/active_record_extensions.rb z następujących czynności:

require 'fix_update_counters'

Dodaj do modeli

Dla każdego modelu chcesz bufory licznik aktualizowany dodatek wywołanie zwrotne:

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

Jeśli licznik został uszkodzony lub zmodyfikowaniu go bezpośrednio przez SQL, można go naprawić.

Zastosowanie:

ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters) 

Przykład 1: ponowne obliczenie liczby pamięci podręcznej na stanowisku id = 17.

Post.reset_counters(17, :comments) 

Source

Przykład 2: Oblicz ponownie liczbę przechowywaną w pamięci podręcznej na wszystkich swoich artykułach.

Article.ids.each { |id| Article.reset_counters(id, :comments) } 
Powiązane problemy